• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Azure / kube-egress-gateway / 16222663082

11 Jul 2025 02:36PM UTC coverage: 80.553% (-0.08%) from 80.63%
16222663082

Pull #1122

github

web-flow
Merge de2b5a1b1 into af50260b0
Pull Request #1122: build: run overlay, nodesubnet, cilium e2e tests

3173 of 3939 relevant lines covered (80.55%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.33
/controllers/manager/staticgatewayconfiguration_controller.go
1
// Copyright (c) Microsoft Corporation.
2
// Licensed under the MIT license.
3

4
package manager
5

6
import (
7
        "context"
8
        "fmt"
9
        "os"
10
        "strings"
11

12
        "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
13
        corev1 "k8s.io/api/core/v1"
14
        apierrors "k8s.io/apimachinery/pkg/api/errors"
15
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
        "k8s.io/apimachinery/pkg/runtime/schema"
17
        "k8s.io/apimachinery/pkg/util/validation/field"
18
        "k8s.io/client-go/tools/record"
19
        ctrl "sigs.k8s.io/controller-runtime"
20
        "sigs.k8s.io/controller-runtime/pkg/builder"
21
        "sigs.k8s.io/controller-runtime/pkg/client"
22
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23
        "sigs.k8s.io/controller-runtime/pkg/event"
24
        "sigs.k8s.io/controller-runtime/pkg/handler"
25
        "sigs.k8s.io/controller-runtime/pkg/log"
26
        "sigs.k8s.io/controller-runtime/pkg/predicate"
27
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
28

29
        egressgatewayv1alpha1 "github.com/Azure/kube-egress-gateway/api/v1alpha1"
30
        "github.com/Azure/kube-egress-gateway/pkg/consts"
31
        "github.com/Azure/kube-egress-gateway/pkg/metrics"
32
)
33

34
var _ reconcile.Reconciler = &StaticGatewayConfigurationReconciler{}
35

36
// StaticGatewayConfigurationReconciler reconciles gateway loadBalancer according to a StaticGatewayConfiguration object
37
type StaticGatewayConfigurationReconciler struct {
38
        client.Client
39
        SecretNamespace string
40
        Recorder        record.EventRecorder
41
}
42

43
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
44
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
45
//+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations,verbs=get;list;watch;create;update;patch;delete
46
//+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations/status,verbs=get;update;patch
47
//+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=staticgatewayconfigurations/finalizers,verbs=update
48
//+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=gatewaylbconfigurations,verbs=get;list;watch;create;update;patch;delete
49
//+kubebuilder:rbac:groups=egressgateway.kubernetes.azure.com,resources=gatewaylbconfigurations/status,verbs=get;update;patch
50

51
// Reconcile is part of the main kubernetes reconciliation loop which aims to
52
// move the current state of the cluster closer to the desired state.
53
func (r *StaticGatewayConfigurationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
54
        log := log.FromContext(ctx)
1✔
55

1✔
56
        // Fetch the StaticGatewayConfiguration instance.
1✔
57
        gwConfig := &egressgatewayv1alpha1.StaticGatewayConfiguration{}
1✔
58
        if err := r.Get(ctx, req.NamespacedName, gwConfig); err != nil {
1✔
59
                if apierrors.IsNotFound(err) {
×
60
                        // Object not found, return.
×
61
                        return ctrl.Result{}, nil
×
62
                }
×
63
                log.Error(err, "unable to fetch StaticGatewayConfiguration instance")
×
64
                return ctrl.Result{}, err
×
65
        }
66

67
        if !gwConfig.ObjectMeta.DeletionTimestamp.IsZero() {
2✔
68
                // Clean up staticGatewayConfiguration
1✔
69
                return ctrl.Result{}, r.ensureDeleted(ctx, gwConfig)
1✔
70
        }
1✔
71

72
        return ctrl.Result{}, r.reconcile(ctx, gwConfig)
1✔
73
}
74

75
// SetupWithManager sets up the controller with the Manager.
76
func (r *StaticGatewayConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error {
1✔
77
        secretPredicate := predicate.Funcs{
1✔
78
                CreateFunc: func(e event.CreateEvent) bool {
2✔
79
                        // no need to trigger reconcile for secrets creation
1✔
80
                        return false
1✔
81
                },
1✔
82
                UpdateFunc: func(e event.UpdateEvent) bool {
×
83
                        return strings.EqualFold(e.ObjectOld.GetNamespace(), r.SecretNamespace)
×
84
                },
×
85
                DeleteFunc: func(e event.DeleteEvent) bool {
1✔
86
                        return strings.EqualFold(e.Object.GetNamespace(), r.SecretNamespace)
1✔
87
                },
1✔
88
                GenericFunc: func(e event.GenericEvent) bool {
×
89
                        return false
×
90
                },
×
91
        }
92
        return ctrl.NewControllerManagedBy(mgr).
1✔
93
                For(&egressgatewayv1alpha1.StaticGatewayConfiguration{}).
1✔
94
                Owns(&egressgatewayv1alpha1.GatewayLBConfiguration{}).
1✔
95
                // generated secrets created in the dedicated namespace
1✔
96
                Watches(&corev1.Secret{}, enqueueOwningSGCFromLabels(), builder.WithPredicates(secretPredicate)).
1✔
97
                Complete(r)
1✔
98
}
99

100
func enqueueOwningSGCFromLabels() handler.EventHandler {
1✔
101
        return handler.EnqueueRequestsFromMapFunc(func(_ context.Context, o client.Object) []reconcile.Request {
2✔
102
                labels := o.GetLabels()
1✔
103
                if labels == nil {
1✔
104
                        return nil
×
105
                }
×
106

107
                owningSGCNamespace, foundNamespace := labels[consts.OwningSGCNamespaceLabel]
1✔
108
                owningSGCName, foundName := labels[consts.OwningSGCNameLabel]
1✔
109

1✔
110
                if !foundNamespace || !foundName {
1✔
111
                        return nil
×
112
                }
×
113

114
                return []reconcile.Request{
1✔
115
                        {
1✔
116
                                NamespacedName: client.ObjectKey{
1✔
117
                                        Name:      owningSGCName,
1✔
118
                                        Namespace: owningSGCNamespace,
1✔
119
                                },
1✔
120
                        },
1✔
121
                }
1✔
122
        })
123
}
124

125
func (r *StaticGatewayConfigurationReconciler) reconcile(
126
        ctx context.Context,
127
        gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration,
128
) error {
1✔
129
        log := log.FromContext(ctx)
1✔
130
        log.Info(fmt.Sprintf("Reconciling staticGatewayConfiguration %s/%s", gwConfig.Namespace, gwConfig.Name))
1✔
131

1✔
132
        mc := metrics.NewMetricsContext(
1✔
133
                os.Getenv(consts.PodNamespaceEnvKey),
1✔
134
                "reconcile_static_gateway_configuration",
1✔
135
                "n/a",
1✔
136
                "n/a",
1✔
137
                strings.ToLower(fmt.Sprintf("%s/%s", gwConfig.Namespace, gwConfig.Name)),
1✔
138
        ) // no subscription_id/resource_group for SGC reconciler
1✔
139
        succeeded := false
1✔
140
        defer func() { mc.ObserveControllerReconcileMetrics(succeeded) }()
2✔
141

142
        if err := validate(gwConfig); err != nil {
1✔
143
                r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "InvalidSpec", err.Error())
×
144
                return err
×
145
        }
×
146

147
        if !controllerutil.ContainsFinalizer(gwConfig, consts.SGCFinalizerName) {
2✔
148
                log.Info("Adding finalizer")
1✔
149
                controllerutil.AddFinalizer(gwConfig, consts.SGCFinalizerName)
1✔
150
                err := r.Update(ctx, gwConfig)
1✔
151
                if err != nil {
1✔
152
                        log.Error(err, "failed to add finalizer")
×
153
                        return err
×
154
                }
×
155
        }
156

157
        _, err := controllerutil.CreateOrPatch(ctx, r, gwConfig, func() error {
2✔
158
                // reconcile wireguard keypair
1✔
159
                if err := r.reconcileWireguardKey(ctx, gwConfig); err != nil {
1✔
160
                        log.Error(err, "failed to reconcile wireguard key")
×
161
                        r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "ReconcileError", err.Error())
×
162
                        return err
×
163
                }
×
164

165
                // reconcile lbconfig
166
                if err := r.reconcileGatewayLBConfig(ctx, gwConfig); err != nil {
1✔
167
                        log.Error(err, "failed to reconcile gateway LB configuration")
×
168
                        r.Recorder.Event(gwConfig, corev1.EventTypeWarning, "ReconcileError", err.Error())
×
169
                        return err
×
170
                }
×
171

172
                return nil
1✔
173
        })
174

175
        prefix, reconcileStatus := "<pending>", "Reconciling"
1✔
176
        if gwConfig.Status.EgressIpPrefix != "" {
2✔
177
                prefix, reconcileStatus = gwConfig.Status.EgressIpPrefix, "Reconciled"
1✔
178
        }
1✔
179
        r.Recorder.Eventf(gwConfig, corev1.EventTypeNormal, reconcileStatus, "StaticGatewayConfiguration provisioned with egress prefix %s", prefix)
1✔
180
        log.Info("staticGatewayConfiguration reconciled")
1✔
181
        succeeded = true
1✔
182
        return err
1✔
183
}
184

185
func (r *StaticGatewayConfigurationReconciler) ensureDeleted(
186
        ctx context.Context,
187
        gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration,
188
) error {
1✔
189
        log := log.FromContext(ctx)
1✔
190
        log.Info(fmt.Sprintf("Reconciling staticGatewayConfiguration deletion %s/%s", gwConfig.Namespace, gwConfig.Name))
1✔
191

1✔
192
        if !controllerutil.ContainsFinalizer(gwConfig, consts.SGCFinalizerName) {
2✔
193
                log.Info("gwConfig does not have finalizer, no additional cleanup needed")
1✔
194
                return nil
1✔
195
        }
1✔
196

197
        mc := metrics.NewMetricsContext(
1✔
198
                os.Getenv(consts.PodNamespaceEnvKey),
1✔
199
                "delete_static_gateway_configuration",
1✔
200
                "n/a",
1✔
201
                "n/a",
1✔
202
                strings.ToLower(fmt.Sprintf("%s/%s", gwConfig.Namespace, gwConfig.Name)),
1✔
203
        ) // no subscription_id/resource_group for SGC reconciler
1✔
204
        succeeded := false
1✔
205
        defer func() { mc.ObserveControllerReconcileMetrics(succeeded) }()
2✔
206

207
        secretDeleted := false
1✔
208
        log.Info("Deleting wireguard key")
1✔
209
        secret := &corev1.Secret{
1✔
210
                ObjectMeta: metav1.ObjectMeta{
1✔
211
                        Name:      fmt.Sprintf("sgw-%s", string(gwConfig.UID)),
1✔
212
                        Namespace: r.SecretNamespace,
1✔
213
                },
1✔
214
        }
1✔
215
        if err := r.Delete(ctx, secret); err != nil {
2✔
216
                if !apierrors.IsNotFound(err) {
1✔
217
                        log.Error(err, "failed to delete existing gateway LB configuration")
×
218
                        return err
×
219
                } else {
1✔
220
                        secretDeleted = true
1✔
221
                }
1✔
222
        }
223

224
        lbConfigDeleted := false
1✔
225
        log.Info("Deleting gateway LB configuration")
1✔
226
        lbConfig := &egressgatewayv1alpha1.GatewayLBConfiguration{
1✔
227
                ObjectMeta: metav1.ObjectMeta{
1✔
228
                        Name:      gwConfig.Name,
1✔
229
                        Namespace: gwConfig.Namespace,
1✔
230
                },
1✔
231
        }
1✔
232
        if err := r.Delete(ctx, lbConfig); err != nil {
2✔
233
                if !apierrors.IsNotFound(err) {
1✔
234
                        log.Error(err, "failed to delete existing gateway LB configuration")
×
235
                        return err
×
236
                } else {
1✔
237
                        lbConfigDeleted = true
1✔
238
                }
1✔
239
        }
240

241
        if secretDeleted && lbConfigDeleted {
2✔
242
                log.Info("Secret and LBConfig are deleted, removing finalizer")
1✔
243
                controllerutil.RemoveFinalizer(gwConfig, consts.SGCFinalizerName)
1✔
244
                if err := r.Update(ctx, gwConfig); err != nil {
1✔
245
                        log.Error(err, "failed to remove finalizer")
×
246
                        return err
×
247
                }
×
248
        }
249

250
        log.Info("staticGatewayConfiguration deletion reconciled")
1✔
251
        succeeded = true
1✔
252
        return nil
1✔
253
}
254

255
func validate(gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration) error {
1✔
256
        // need to validate either GatewayNodepoolName or GatewayVmssProfile is provided, but not both
1✔
257
        var allErrs field.ErrorList
1✔
258

1✔
259
        if gwConfig.Spec.GatewayNodepoolName == "" && vmssProfileIsEmpty(gwConfig) {
2✔
260
                allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewaynodepoolname"),
1✔
261
                        fmt.Sprintf("GatewayNodepoolName: %s, GatewayVmssProfile: %#v", gwConfig.Spec.GatewayNodepoolName, gwConfig.Spec.GatewayVmssProfile),
1✔
262
                        "Either GatewayNodepoolName or GatewayVmssProfile must be provided"))
1✔
263
        }
1✔
264

265
        if gwConfig.Spec.GatewayNodepoolName != "" && !vmssProfileIsEmpty(gwConfig) {
2✔
266
                allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewaynodepoolname"),
1✔
267
                        fmt.Sprintf("GatewayNodepoolName: %s, GatewayVmssProfile: %#v", gwConfig.Spec.GatewayNodepoolName, gwConfig.Spec.GatewayVmssProfile),
1✔
268
                        "Only one of GatewayNodepoolName and GatewayVmssProfile should be provided"))
1✔
269
        }
1✔
270

271
        if !vmssProfileIsEmpty(gwConfig) {
2✔
272
                if gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup == "" {
2✔
273
                        allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("vmssresourcegroup"),
1✔
274
                                gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup,
1✔
275
                                "Gateway vmss resource group is empty"))
1✔
276
                }
1✔
277
                if gwConfig.Spec.GatewayVmssProfile.VmssName == "" {
2✔
278
                        allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("vmssname"),
1✔
279
                                gwConfig.Spec.GatewayVmssProfile.VmssName,
1✔
280
                                "Gateway vmss name is empty"))
1✔
281
                }
1✔
282
                if gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize < 0 || gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize > 31 {
2✔
283
                        allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("gatewayvmssprofile").Child("publicipprefixsize"),
1✔
284
                                gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize,
1✔
285
                                "Gateway vmss public ip prefix size should be between 0 and 31 inclusively"))
1✔
286
                }
1✔
287
        }
288

289
        if !gwConfig.Spec.ProvisionPublicIps && gwConfig.Spec.PublicIpPrefixId != "" {
2✔
290
                allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("publicipprefixid"),
1✔
291
                        gwConfig.Spec.PublicIpPrefixId,
1✔
292
                        "PublicIpPrefixId should be empty when ProvisionPublicIps is false"))
1✔
293
        }
1✔
294

295
        if len(allErrs) == 0 {
2✔
296
                return nil
1✔
297
        }
1✔
298
        return apierrors.NewInvalid(
1✔
299
                schema.GroupKind{Group: "egressgateway.kubernetes.azure.com", Kind: "StaticGatewayConfiguration"},
1✔
300
                gwConfig.Name, allErrs)
1✔
301
}
302

303
func vmssProfileIsEmpty(gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration) bool {
1✔
304
        return gwConfig.Spec.GatewayVmssProfile.VmssResourceGroup == "" &&
1✔
305
                gwConfig.Spec.GatewayVmssProfile.VmssName == "" &&
1✔
306
                gwConfig.Spec.GatewayVmssProfile.PublicIpPrefixSize == 0
1✔
307
}
1✔
308

309
func (r *StaticGatewayConfigurationReconciler) reconcileWireguardKey(
310
        ctx context.Context,
311
        gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration,
312
) error {
1✔
313
        log := log.FromContext(ctx)
1✔
314

1✔
315
        secret := &corev1.Secret{
1✔
316
                ObjectMeta: metav1.ObjectMeta{
1✔
317
                        Name:      fmt.Sprintf("sgw-%s", string(gwConfig.UID)),
1✔
318
                        Namespace: r.SecretNamespace,
1✔
319
                },
1✔
320
        }
1✔
321
        if _, err := controllerutil.CreateOrUpdate(ctx, r, secret, func() error {
2✔
322
                if secret.Labels == nil {
2✔
323
                        secret.Labels = make(map[string]string)
1✔
324
                }
1✔
325
                if sgcNS, ok := secret.Labels[consts.OwningSGCNamespaceLabel]; !ok || sgcNS != gwConfig.Namespace {
2✔
326
                        secret.Labels[consts.OwningSGCNamespaceLabel] = gwConfig.Namespace
1✔
327
                }
1✔
328
                if sgcName, ok := secret.Labels[consts.OwningSGCNameLabel]; !ok || sgcName != gwConfig.Name {
2✔
329
                        secret.Labels[consts.OwningSGCNameLabel] = gwConfig.Name
1✔
330
                }
1✔
331

332
                if secret.Data == nil {
2✔
333
                        secret.Data = make(map[string][]byte)
1✔
334
                }
1✔
335
                if _, ok := secret.Data[consts.WireguardPrivateKeyName]; !ok {
2✔
336
                        // create new private key
1✔
337
                        wgPrivateKey, err := wgtypes.GeneratePrivateKey()
1✔
338
                        if err != nil {
1✔
339
                                log.Error(err, "failed to generate wireguard private key")
×
340
                                return err
×
341
                        }
×
342

343
                        secret.Data[consts.WireguardPrivateKeyName] = []byte(wgPrivateKey.String())
1✔
344
                        secret.Data[consts.WireguardPublicKeyName] = []byte(wgPrivateKey.PublicKey().String())
1✔
345
                }
346

347
                return nil
1✔
348
        }); err != nil {
×
349
                log.Error(err, "failed to reconcile wireguard keypair secret")
×
350
                return err
×
351
        }
×
352
        if secret.DeletionTimestamp.IsZero() {
2✔
353
                // Update secret reference
1✔
354
                gwConfig.Status.PrivateKeySecretRef = &corev1.ObjectReference{
1✔
355
                        APIVersion: "v1",
1✔
356
                        Kind:       "Secret",
1✔
357
                        Name:       secret.Name,
1✔
358
                        Namespace:  secret.Namespace,
1✔
359
                }
1✔
360

1✔
361
                // Update public key
1✔
362
                gwConfig.Status.PublicKey = string(secret.Data[consts.WireguardPublicKeyName])
1✔
363
        }
1✔
364

365
        return nil
1✔
366
}
367

368
func (r *StaticGatewayConfigurationReconciler) reconcileGatewayLBConfig(
369
        ctx context.Context,
370
        gwConfig *egressgatewayv1alpha1.StaticGatewayConfiguration,
371
) error {
1✔
372
        log := log.FromContext(ctx)
1✔
373

1✔
374
        // check existence of the gatewayLBConfig
1✔
375
        lbConfig := &egressgatewayv1alpha1.GatewayLBConfiguration{
1✔
376
                ObjectMeta: metav1.ObjectMeta{
1✔
377
                        Name:      gwConfig.Name,
1✔
378
                        Namespace: gwConfig.Namespace,
1✔
379
                },
1✔
380
        }
1✔
381
        if _, err := controllerutil.CreateOrPatch(ctx, r, lbConfig, func() error {
2✔
382
                lbConfig.Spec.GatewayNodepoolName = gwConfig.Spec.GatewayNodepoolName
1✔
383
                lbConfig.Spec.GatewayVmssProfile = gwConfig.Spec.GatewayVmssProfile
1✔
384
                lbConfig.Spec.ProvisionPublicIps = gwConfig.Spec.ProvisionPublicIps
1✔
385
                lbConfig.Spec.PublicIpPrefixId = gwConfig.Spec.PublicIpPrefixId
1✔
386
                return controllerutil.SetControllerReference(gwConfig, lbConfig, r.Client.Scheme())
1✔
387
        }); err != nil {
1✔
388
                log.Error(err, "failed to reconcile gateway lb configuration")
×
389
                return err
×
390
        }
×
391
        if lbConfig.DeletionTimestamp.IsZero() && lbConfig.Status != nil {
2✔
392
                gwConfig.Status.Ip = lbConfig.Status.FrontendIp
1✔
393
                gwConfig.Status.Port = lbConfig.Status.ServerPort
1✔
394
                gwConfig.Status.EgressIpPrefix = lbConfig.Status.EgressIpPrefix
1✔
395
        }
1✔
396

397
        return nil
1✔
398
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc