Skip to content

Commit e066e2d

Browse files
committed
feat: support gateway proxy webhook
Signed-off-by: Ashing Zheng <[email protected]>
1 parent d904d73 commit e066e2d

File tree

2 files changed

+337
-3
lines changed

2 files changed

+337
-3
lines changed

internal/webhook/v1/gatewayproxy_webhook.go

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package v1
1818
import (
1919
"context"
2020
"fmt"
21+
"sort"
22+
"strings"
2123

2224
"k8s.io/apimachinery/pkg/runtime"
2325
"k8s.io/apimachinery/pkg/types"
@@ -63,7 +65,12 @@ func (v *GatewayProxyCustomValidator) ValidateCreate(ctx context.Context, obj ru
6365
}
6466
gatewayProxyLog.Info("Validation for GatewayProxy upon creation", "name", gp.GetName(), "namespace", gp.GetNamespace())
6567

66-
return v.collectWarnings(ctx, gp), nil
68+
warnings := v.collectWarnings(ctx, gp)
69+
if err := v.validateGatewayGroupConflict(ctx, gp); err != nil {
70+
return warnings, err
71+
}
72+
73+
return warnings, nil
6774
}
6875

6976
func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
@@ -73,7 +80,12 @@ func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj
7380
}
7481
gatewayProxyLog.Info("Validation for GatewayProxy upon update", "name", gp.GetName(), "namespace", gp.GetNamespace())
7582

76-
return v.collectWarnings(ctx, gp), nil
83+
warnings := v.collectWarnings(ctx, gp)
84+
if err := v.validateGatewayGroupConflict(ctx, gp); err != nil {
85+
return warnings, err
86+
}
87+
88+
return warnings, nil
7789
}
7890

7991
func (v *GatewayProxyCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
@@ -111,3 +123,114 @@ func (v *GatewayProxyCustomValidator) collectWarnings(ctx context.Context, gp *v
111123

112124
return warnings
113125
}
126+
127+
func (v *GatewayProxyCustomValidator) validateGatewayGroupConflict(ctx context.Context, gp *v1alpha1.GatewayProxy) error {
128+
current := buildGatewayGroupConfig(gp)
129+
if !current.readyForConflict() {
130+
return nil
131+
}
132+
133+
var list v1alpha1.GatewayProxyList
134+
if err := v.Client.List(ctx, &list); err != nil {
135+
gatewayProxyLog.Error(err, "failed to list GatewayProxy objects for conflict detection")
136+
return fmt.Errorf("failed to list existing GatewayProxy resources: %w", err)
137+
}
138+
139+
for _, other := range list.Items {
140+
if other.GetNamespace() == gp.GetNamespace() && other.GetName() == gp.GetName() {
141+
// skip self
142+
continue
143+
}
144+
otherConfig := buildGatewayGroupConfig(&other)
145+
if !otherConfig.readyForConflict() {
146+
continue
147+
}
148+
if current.adminKeyKey != otherConfig.adminKeyKey {
149+
continue
150+
}
151+
if current.serviceKey != "" && current.serviceKey == otherConfig.serviceKey {
152+
return fmt.Errorf("gateway group conflict: GatewayProxy %s/%s and %s/%s both target %s while sharing %s",
153+
gp.GetNamespace(), gp.GetName(),
154+
other.GetNamespace(), other.GetName(),
155+
current.serviceDescription,
156+
current.adminKeyDescription,
157+
)
158+
}
159+
if len(current.endpoints) > 0 && len(otherConfig.endpoints) > 0 {
160+
if overlap := current.endpointOverlap(otherConfig); len(overlap) > 0 {
161+
return fmt.Errorf("gateway group conflict: GatewayProxy %s/%s and %s/%s both target control plane endpoints [%s] while sharing %s",
162+
gp.GetNamespace(), gp.GetName(),
163+
other.GetNamespace(), other.GetName(),
164+
strings.Join(overlap, ", "),
165+
current.adminKeyDescription,
166+
)
167+
}
168+
}
169+
}
170+
171+
return nil
172+
}
173+
174+
type gatewayGroupConfig struct {
175+
adminKeyKey string
176+
adminKeyDescription string
177+
serviceKey string
178+
serviceDescription string
179+
endpoints map[string]struct{}
180+
sortedEndpoints []string
181+
}
182+
183+
func buildGatewayGroupConfig(gp *v1alpha1.GatewayProxy) gatewayGroupConfig {
184+
var cfg gatewayGroupConfig
185+
186+
if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.Type != v1alpha1.ProviderTypeControlPlane || gp.Spec.Provider.ControlPlane == nil {
187+
return cfg
188+
}
189+
190+
cp := gp.Spec.Provider.ControlPlane
191+
192+
if cp.Auth.AdminKey != nil {
193+
if value := strings.TrimSpace(cp.Auth.AdminKey.Value); value != "" {
194+
cfg.adminKeyKey = "value:" + value
195+
cfg.adminKeyDescription = "the same inline AdminKey value"
196+
} else if cp.Auth.AdminKey.ValueFrom != nil && cp.Auth.AdminKey.ValueFrom.SecretKeyRef != nil {
197+
ref := cp.Auth.AdminKey.ValueFrom.SecretKeyRef
198+
cfg.adminKeyKey = fmt.Sprintf("secret:%s/%s:%s", gp.GetNamespace(), ref.Name, ref.Key)
199+
cfg.adminKeyDescription = fmt.Sprintf("AdminKey secret %s/%s key %s", gp.GetNamespace(), ref.Name, ref.Key)
200+
}
201+
}
202+
203+
if cp.Service != nil && cp.Service.Name != "" {
204+
cfg.serviceKey = fmt.Sprintf("service:%s/%s:%d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port)
205+
cfg.serviceDescription = fmt.Sprintf("Service %s/%s port %d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port)
206+
}
207+
208+
if len(cp.Endpoints) > 0 {
209+
cfg.endpoints = make(map[string]struct{}, len(cp.Endpoints))
210+
cfg.sortedEndpoints = append([]string(nil), cp.Endpoints...)
211+
for _, endpoint := range cfg.sortedEndpoints {
212+
cfg.endpoints[endpoint] = struct{}{}
213+
}
214+
sort.Strings(cfg.sortedEndpoints)
215+
}
216+
217+
return cfg
218+
}
219+
220+
func (c gatewayGroupConfig) readyForConflict() bool {
221+
if c.adminKeyKey == "" {
222+
return false
223+
}
224+
return c.serviceKey != "" || len(c.endpoints) > 0
225+
}
226+
227+
func (c gatewayGroupConfig) endpointOverlap(other gatewayGroupConfig) []string {
228+
var overlap []string
229+
for endpoint := range c.endpoints {
230+
if _, ok := other.endpoints[endpoint]; ok {
231+
overlap = append(overlap, endpoint)
232+
}
233+
}
234+
sort.Strings(overlap)
235+
return overlap
236+
}

internal/webhook/v1/gatewayproxy_webhook_test.go

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func newGatewayProxy() *v1alpha1.GatewayProxy {
5454
Provider: &v1alpha1.GatewayProxyProvider{
5555
Type: v1alpha1.ProviderTypeControlPlane,
5656
ControlPlane: &v1alpha1.ControlPlaneProvider{
57-
Service: &v1alpha1.ProviderService{Name: "control-plane"},
57+
Service: &v1alpha1.ProviderService{Name: "control-plane", Port: 9180},
5858
Auth: v1alpha1.ControlPlaneAuth{
5959
Type: v1alpha1.AuthTypeAdminKey,
6060
AdminKey: &v1alpha1.AdminKeyAuth{
@@ -72,6 +72,41 @@ func newGatewayProxy() *v1alpha1.GatewayProxy {
7272
}
7373
}
7474

75+
func newGatewayProxyWithEndpoints(name string, endpoints []string) *v1alpha1.GatewayProxy {
76+
gp := newGatewayProxy()
77+
gp.Name = name
78+
gp.Spec.Provider.ControlPlane.Service = nil
79+
gp.Spec.Provider.ControlPlane.Endpoints = endpoints
80+
return gp
81+
}
82+
83+
func setInlineAdminKey(gp *v1alpha1.GatewayProxy, value string) {
84+
if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
85+
return
86+
}
87+
if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil {
88+
gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{}
89+
}
90+
gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = value
91+
gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = nil
92+
}
93+
94+
func setSecretAdminKey(gp *v1alpha1.GatewayProxy, name, key string) {
95+
if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
96+
return
97+
}
98+
if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil {
99+
gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{}
100+
}
101+
gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = ""
102+
gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = &v1alpha1.AdminKeyValueFrom{
103+
SecretKeyRef: &v1alpha1.SecretKeySelector{
104+
Name: name,
105+
Key: key,
106+
},
107+
}
108+
}
109+
75110
func TestGatewayProxyValidator_MissingService(t *testing.T) {
76111
gp := newGatewayProxy()
77112
gp.Spec.Provider.ControlPlane.Auth.AdminKey = nil
@@ -150,3 +185,179 @@ func TestGatewayProxyValidator_NoWarnings(t *testing.T) {
150185
require.NoError(t, err)
151186
require.Empty(t, warnings)
152187
}
188+
189+
func TestGatewayProxyValidator_DetectsServiceConflict(t *testing.T) {
190+
existing := newGatewayProxy()
191+
existing.Name = "existing"
192+
193+
service := &corev1.Service{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: "control-plane",
196+
Namespace: "default",
197+
},
198+
}
199+
secret := &corev1.Secret{
200+
ObjectMeta: metav1.ObjectMeta{
201+
Name: "admin-key",
202+
Namespace: "default",
203+
},
204+
Data: map[string][]byte{
205+
"token": []byte("value"),
206+
},
207+
}
208+
209+
validator := buildGatewayProxyValidator(t, existing, service, secret)
210+
211+
candidate := newGatewayProxy()
212+
candidate.Name = "candidate"
213+
214+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
215+
require.Error(t, err)
216+
require.Len(t, warnings, 0)
217+
require.Contains(t, err.Error(), "gateway group conflict")
218+
require.Contains(t, err.Error(), "Service default/control-plane port 9180")
219+
require.Contains(t, err.Error(), "AdminKey secret default/admin-key key token")
220+
}
221+
222+
func TestGatewayProxyValidator_DetectsEndpointConflict(t *testing.T) {
223+
existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
224+
secret := &corev1.Secret{
225+
ObjectMeta: metav1.ObjectMeta{
226+
Name: "admin-key",
227+
Namespace: "default",
228+
},
229+
Data: map[string][]byte{
230+
"token": []byte("value"),
231+
},
232+
}
233+
validator := buildGatewayProxyValidator(t, existing, secret)
234+
235+
candidate := newGatewayProxyWithEndpoints("candidate", []string{"https://10.0.0.1:9443", "https://127.0.0.1:9443"})
236+
237+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
238+
require.Error(t, err)
239+
require.Len(t, warnings, 0)
240+
require.Contains(t, err.Error(), "gateway group conflict")
241+
require.Contains(t, err.Error(), "endpoints [https://10.0.0.1:9443, https://127.0.0.1:9443]")
242+
require.Contains(t, err.Error(), "AdminKey secret default/admin-key key token")
243+
}
244+
245+
func TestGatewayProxyValidator_AllowsDistinctGatewayGroups(t *testing.T) {
246+
existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443"})
247+
secret := &corev1.Secret{
248+
ObjectMeta: metav1.ObjectMeta{
249+
Name: "admin-key",
250+
Namespace: "default",
251+
},
252+
Data: map[string][]byte{
253+
"token": []byte("value"),
254+
},
255+
}
256+
service := &corev1.Service{
257+
ObjectMeta: metav1.ObjectMeta{
258+
Name: "control-plane",
259+
Namespace: "default",
260+
},
261+
}
262+
validator := buildGatewayProxyValidator(t, existing, secret, service)
263+
264+
candidate := newGatewayProxy()
265+
candidate.Name = "candidate"
266+
candidate.Spec.Provider.ControlPlane.Service = &v1alpha1.ProviderService{
267+
Name: "control-plane",
268+
Port: 9180,
269+
}
270+
271+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
272+
require.NoError(t, err)
273+
require.Empty(t, warnings)
274+
}
275+
276+
func TestGatewayProxyValidator_AllowsServiceConflictWithDifferentAdminSecret(t *testing.T) {
277+
existing := newGatewayProxy()
278+
existing.Name = "existing"
279+
280+
candidate := newGatewayProxy()
281+
candidate.Name = "candidate"
282+
setSecretAdminKey(candidate, "admin-key-alt", "token")
283+
284+
service := &corev1.Service{
285+
ObjectMeta: metav1.ObjectMeta{
286+
Name: "control-plane",
287+
Namespace: "default",
288+
},
289+
}
290+
existingSecret := &corev1.Secret{
291+
ObjectMeta: metav1.ObjectMeta{
292+
Name: "admin-key",
293+
Namespace: "default",
294+
},
295+
Data: map[string][]byte{
296+
"token": []byte("value"),
297+
},
298+
}
299+
altSecret := &corev1.Secret{
300+
ObjectMeta: metav1.ObjectMeta{
301+
Name: "admin-key-alt",
302+
Namespace: "default",
303+
},
304+
Data: map[string][]byte{
305+
"token": []byte("value"),
306+
},
307+
}
308+
309+
validator := buildGatewayProxyValidator(t, existing, service, existingSecret, altSecret)
310+
311+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
312+
require.NoError(t, err)
313+
require.Empty(t, warnings)
314+
}
315+
316+
func TestGatewayProxyValidator_DetectsInlineAdminKeyConflict(t *testing.T) {
317+
existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
318+
setInlineAdminKey(existing, "inline-cred")
319+
320+
candidate := newGatewayProxyWithEndpoints("candidate", []string{"https://10.0.0.1:9443"})
321+
setInlineAdminKey(candidate, "inline-cred")
322+
323+
validator := buildGatewayProxyValidator(t, existing)
324+
325+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
326+
require.Error(t, err)
327+
require.Len(t, warnings, 0)
328+
require.Contains(t, err.Error(), "gateway group conflict")
329+
require.Contains(t, err.Error(), "control plane endpoints [https://10.0.0.1:9443]")
330+
require.Contains(t, err.Error(), "inline AdminKey value")
331+
}
332+
333+
func TestGatewayProxyValidator_AllowsEndpointOverlapWithDifferentAdminKey(t *testing.T) {
334+
existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
335+
336+
candidate := newGatewayProxyWithEndpoints("candidate", []string{"https://10.0.0.1:9443", "https://192.168.0.1:9443"})
337+
setSecretAdminKey(candidate, "admin-key-alt", "token")
338+
339+
existingSecret := &corev1.Secret{
340+
ObjectMeta: metav1.ObjectMeta{
341+
Name: "admin-key",
342+
Namespace: "default",
343+
},
344+
Data: map[string][]byte{
345+
"token": []byte("value"),
346+
},
347+
}
348+
altSecret := &corev1.Secret{
349+
ObjectMeta: metav1.ObjectMeta{
350+
Name: "admin-key-alt",
351+
Namespace: "default",
352+
},
353+
Data: map[string][]byte{
354+
"token": []byte("value"),
355+
},
356+
}
357+
358+
validator := buildGatewayProxyValidator(t, existing, existingSecret, altSecret)
359+
360+
warnings, err := validator.ValidateCreate(context.Background(), candidate)
361+
require.NoError(t, err)
362+
require.Empty(t, warnings)
363+
}

0 commit comments

Comments
 (0)