Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-manager-mk2/controllers/maintenance_controller.go
2498 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package controllers
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"os"
12
"sync"
13
"time"
14
15
"github.com/gitpod-io/gitpod/ws-manager/api/config"
16
"github.com/prometheus/client_golang/prometheus"
17
corev1 "k8s.io/api/core/v1"
18
"k8s.io/apimachinery/pkg/api/errors"
19
"k8s.io/apimachinery/pkg/types"
20
ctrl "sigs.k8s.io/controller-runtime"
21
"sigs.k8s.io/controller-runtime/pkg/client"
22
"sigs.k8s.io/controller-runtime/pkg/controller"
23
"sigs.k8s.io/controller-runtime/pkg/handler"
24
"sigs.k8s.io/controller-runtime/pkg/log"
25
"sigs.k8s.io/controller-runtime/pkg/predicate"
26
"sigs.k8s.io/controller-runtime/pkg/source"
27
)
28
29
var (
30
configMapKey = types.NamespacedName{Name: "ws-manager-mk2-maintenance-mode", Namespace: "default"}
31
// lookupOnce is used for the first call to IsEnabled to load the maintenance mode state by looking
32
// up the ConfigMap, as it's possible we haven't received a reconcile event yet to load its state.
33
lookupOnce sync.Once
34
)
35
36
func NewMaintenanceReconciler(c client.Client, reg prometheus.Registerer) (*MaintenanceReconciler, error) {
37
r := &MaintenanceReconciler{
38
Client: c,
39
enabledUntil: nil,
40
}
41
42
gauge := newMaintenanceEnabledGauge(r)
43
reg.MustRegister(gauge)
44
45
return r, nil
46
}
47
48
type MaintenanceReconciler struct {
49
client.Client
50
51
enabledUntil *time.Time
52
}
53
54
func (r *MaintenanceReconciler) IsEnabled(ctx context.Context) bool {
55
// On the first call, we load the maintenance mode state from the ConfigMap,
56
// as it's possible we haven't reconciled it yet.
57
lookupOnce.Do(func() {
58
if err := r.loadFromCM(ctx, configMapKey); err != nil {
59
log.FromContext(ctx).Error(err, "cannot load maintenance mode config")
60
}
61
})
62
63
return r.enabledUntil != nil && time.Now().Before(*r.enabledUntil)
64
}
65
66
//+kubebuilder:rbac:groups=core,resources=configmap,verbs=get;list;watch
67
68
func (r *MaintenanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
69
if req.Name != configMapKey.Name || req.Namespace != configMapKey.Namespace {
70
// Ignore all other configmaps.
71
return ctrl.Result{}, nil
72
}
73
74
return ctrl.Result{}, r.loadFromCM(ctx, req.NamespacedName)
75
}
76
77
func (r *MaintenanceReconciler) loadFromCM(ctx context.Context, key types.NamespacedName) error {
78
log := log.FromContext(ctx)
79
80
var cm corev1.ConfigMap
81
if err := r.Get(ctx, key, &cm); err != nil {
82
if errors.IsNotFound(err) {
83
// ConfigMap does not exist, disable maintenance mode.
84
r.setEnabledUntil(ctx, nil)
85
return nil
86
}
87
88
log.Error(err, "unable to fetch configmap")
89
return fmt.Errorf("failed to fetch configmap: %w", err)
90
}
91
92
configJson, ok := cm.Data["config.json"]
93
if !ok {
94
log.Info("missing config.json, setting maintenance mode as disabled")
95
r.setEnabledUntil(ctx, nil)
96
return nil
97
}
98
99
var cfg config.MaintenanceConfig
100
if err := json.Unmarshal([]byte(configJson), &cfg); err != nil {
101
log.Error(err, "failed to unmarshal maintenance config, setting maintenance mode as disabled")
102
r.setEnabledUntil(ctx, nil)
103
return nil
104
}
105
106
r.setEnabledUntil(ctx, cfg.EnabledUntil)
107
return nil
108
}
109
110
func (r *MaintenanceReconciler) setEnabledUntil(ctx context.Context, enabledUntil *time.Time) {
111
if enabledUntil == r.enabledUntil {
112
// Nothing to do.
113
return
114
}
115
116
r.enabledUntil = enabledUntil
117
log.FromContext(ctx).Info("maintenance mode state change", "enabledUntil", enabledUntil)
118
}
119
120
func (r *MaintenanceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
121
// We need to use an unmanaged controller to avoid issues when the pod is in standby mode.
122
// In that scenario, the controllers are not started and don't watch changes and only
123
// observe the maintenance mode during the initialization.
124
c, err := controller.NewUnmanaged("maintenance-controller", mgr, controller.Options{Reconciler: r})
125
if err != nil {
126
return err
127
}
128
129
go func() {
130
err = c.Start(ctx)
131
if err != nil {
132
log.FromContext(ctx).Error(err, "cannot start maintenance reconciler")
133
os.Exit(1)
134
}
135
}()
136
137
return c.Watch(source.Kind(mgr.GetCache(), &corev1.ConfigMap{}, &handler.TypedEnqueueRequestForObject[*corev1.ConfigMap]{}, predicate.NewTypedPredicateFuncs(filterMaintenanceModeConfigMap)))
138
}
139
140
func filterMaintenanceModeConfigMap(obj *corev1.ConfigMap) bool {
141
if obj == nil {
142
return false
143
}
144
return obj.GetName() == configMapKey.Name && obj.GetNamespace() == configMapKey.Namespace
145
}
146
147