Skip to content

Commit b306394

Browse files
committed
feat: add secret/service resource checker for webhook
Signed-off-by: Ashing Zheng <[email protected]>
1 parent 5bb2afd commit b306394

File tree

9 files changed

+834
-11
lines changed

9 files changed

+834
-11
lines changed

internal/manager/webhooks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) error {
3838
if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != nil {
3939
return err
4040
}
41+
if err := webhookv1.SetupHTTPRouteWebhookWithManager(mgr); err != nil {
42+
return err
43+
}
44+
if err := webhookv1.SetupGRPCRouteWebhookWithManager(mgr); err != nil {
45+
return err
46+
}
4147
if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err != nil {
4248
return err
4349
}

internal/webhook/v1/gateway_webhook.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import (
1919
"context"
2020
"fmt"
2121

22+
corev1 "k8s.io/api/core/v1"
2223
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2324
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/types"
2426
ctrl "sigs.k8s.io/controller-runtime"
2527
"sigs.k8s.io/controller-runtime/pkg/client"
2628
logf "sigs.k8s.io/controller-runtime/pkg/log"
@@ -31,6 +33,7 @@ import (
3133
v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
3234
"github.com/apache/apisix-ingress-controller/internal/controller/config"
3335
internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
36+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
3437
)
3538

3639
// nolint:unused
@@ -40,7 +43,7 @@ var gatewaylog = logf.Log.WithName("gateway-resource")
4043
// SetupGatewayWebhookWithManager registers the webhook for Gateway in the manager.
4144
func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error {
4245
return ctrl.NewWebhookManagedBy(mgr).For(&gatewaynetworkingk8siov1.Gateway{}).
43-
WithValidator(&GatewayCustomValidator{Client: mgr.GetClient()}).
46+
WithValidator(NewGatewayCustomValidator(mgr.GetClient())).
4447
Complete()
4548
}
4649

@@ -54,11 +57,19 @@ func SetupGatewayWebhookWithManager(mgr ctrl.Manager) error {
5457
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
5558
// as this struct is used only for temporary operations and does not need to be deeply copied.
5659
type GatewayCustomValidator struct {
57-
Client client.Client
60+
Client client.Client
61+
checker reference.Checker
5862
}
5963

6064
var _ webhook.CustomValidator = &GatewayCustomValidator{}
6165

66+
func NewGatewayCustomValidator(c client.Client) *GatewayCustomValidator {
67+
return &GatewayCustomValidator{
68+
Client: c,
69+
checker: reference.NewChecker(c, gatewaylog),
70+
}
71+
}
72+
6273
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Gateway.
6374
func (v *GatewayCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
6475
gateway, ok := obj.(*gatewaynetworkingk8siov1.Gateway)
@@ -68,6 +79,7 @@ func (v *GatewayCustomValidator) ValidateCreate(ctx context.Context, obj runtime
6879
gatewaylog.Info("Validation for Gateway upon creation", "name", gateway.GetName())
6980

7081
warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
82+
warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
7183

7284
return warnings, nil
7385
}
@@ -81,6 +93,7 @@ func (v *GatewayCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new
8193
gatewaylog.Info("Validation for Gateway upon update", "name", gateway.GetName())
8294

8395
warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
96+
warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
8497

8598
return warnings, nil
8699
}
@@ -90,6 +103,53 @@ func (v *GatewayCustomValidator) ValidateDelete(_ context.Context, obj runtime.O
90103
return nil, nil
91104
}
92105

106+
func (v *GatewayCustomValidator) collectReferenceWarnings(ctx context.Context, gateway *gatewaynetworkingk8siov1.Gateway) admission.Warnings {
107+
if gateway == nil {
108+
return nil
109+
}
110+
111+
var warnings admission.Warnings
112+
secretVisited := make(map[types.NamespacedName]struct{})
113+
114+
addSecretWarning := func(nn types.NamespacedName) {
115+
if nn.Name == "" || nn.Namespace == "" {
116+
return
117+
}
118+
if _, seen := secretVisited[nn]; seen {
119+
return
120+
}
121+
secretVisited[nn] = struct{}{}
122+
warnings = append(warnings, v.checker.Secret(ctx, reference.SecretRef{
123+
Object: gateway,
124+
NamespacedName: nn,
125+
})...)
126+
}
127+
128+
for _, listener := range gateway.Spec.Listeners {
129+
if listener.TLS == nil {
130+
continue
131+
}
132+
for _, ref := range listener.TLS.CertificateRefs {
133+
if ref.Kind != nil && *ref.Kind != internaltypes.KindSecret {
134+
continue
135+
}
136+
if ref.Group != nil && string(*ref.Group) != corev1.GroupName {
137+
continue
138+
}
139+
nn := types.NamespacedName{
140+
Namespace: gateway.GetNamespace(),
141+
Name: string(ref.Name),
142+
}
143+
if ref.Namespace != nil && *ref.Namespace != "" {
144+
nn.Namespace = string(*ref.Namespace)
145+
}
146+
addSecretWarning(nn)
147+
}
148+
}
149+
150+
return warnings
151+
}
152+
93153
func (v *GatewayCustomValidator) warnIfMissingGatewayProxyForGateway(ctx context.Context, gateway *gatewaynetworkingk8siov1.Gateway) admission.Warnings {
94154
var warnings admission.Warnings
95155

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package v1
17+
18+
import (
19+
"context"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
28+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
29+
gatewaynetworkingk8siov1 "sigs.k8s.io/gateway-api/apis/v1"
30+
31+
"github.com/apache/apisix-ingress-controller/internal/controller/config"
32+
)
33+
34+
func buildGatewayValidator(t *testing.T, objects ...runtime.Object) *GatewayCustomValidator {
35+
t.Helper()
36+
37+
scheme := runtime.NewScheme()
38+
require.NoError(t, clientgoscheme.AddToScheme(scheme))
39+
require.NoError(t, gatewaynetworkingk8siov1.Install(scheme))
40+
41+
builder := fake.NewClientBuilder().WithScheme(scheme)
42+
if len(objects) > 0 {
43+
builder = builder.WithRuntimeObjects(objects...)
44+
}
45+
46+
return NewGatewayCustomValidator(builder.Build())
47+
}
48+
49+
func TestGatewayCustomValidator_WarnsWhenTLSSecretMissing(t *testing.T) {
50+
className := gatewaynetworkingk8siov1.ObjectName("apisix")
51+
gatewayClass := &gatewaynetworkingk8siov1.GatewayClass{
52+
ObjectMeta: metav1.ObjectMeta{Name: string(className)},
53+
Spec: gatewaynetworkingk8siov1.GatewayClassSpec{
54+
ControllerName: gatewaynetworkingk8siov1.GatewayController(config.ControllerConfig.ControllerName),
55+
},
56+
}
57+
validator := buildGatewayValidator(t, gatewayClass)
58+
59+
gateway := &gatewaynetworkingk8siov1.Gateway{
60+
ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "default"},
61+
Spec: gatewaynetworkingk8siov1.GatewaySpec{
62+
GatewayClassName: className,
63+
Listeners: []gatewaynetworkingk8siov1.Listener{{
64+
Name: "https",
65+
Port: 443,
66+
Protocol: gatewaynetworkingk8siov1.HTTPSProtocolType,
67+
TLS: &gatewaynetworkingk8siov1.GatewayTLSConfig{
68+
CertificateRefs: []gatewaynetworkingk8siov1.SecretObjectReference{{
69+
Name: "missing-cert",
70+
}},
71+
},
72+
}},
73+
},
74+
}
75+
76+
warnings, err := validator.ValidateCreate(context.Background(), gateway)
77+
require.NoError(t, err)
78+
require.Len(t, warnings, 1)
79+
assert.Equal(t, warnings[0], "Referenced Secret 'default/missing-cert' not found")
80+
}
81+
82+
func TestGatewayCustomValidator_NoWarningsWhenSecretExists(t *testing.T) {
83+
className := gatewaynetworkingk8siov1.ObjectName("apisix")
84+
gatewayClass := &gatewaynetworkingk8siov1.GatewayClass{
85+
ObjectMeta: metav1.ObjectMeta{Name: string(className)},
86+
Spec: gatewaynetworkingk8siov1.GatewayClassSpec{
87+
ControllerName: gatewaynetworkingk8siov1.GatewayController(config.ControllerConfig.ControllerName),
88+
},
89+
}
90+
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls-cert", Namespace: "default"}}
91+
validator := buildGatewayValidator(t, gatewayClass, secret)
92+
93+
gateway := &gatewaynetworkingk8siov1.Gateway{
94+
ObjectMeta: metav1.ObjectMeta{Name: "example", Namespace: "default"},
95+
Spec: gatewaynetworkingk8siov1.GatewaySpec{
96+
GatewayClassName: className,
97+
Listeners: []gatewaynetworkingk8siov1.Listener{{
98+
Name: "https",
99+
Port: 443,
100+
Protocol: gatewaynetworkingk8siov1.HTTPSProtocolType,
101+
TLS: &gatewaynetworkingk8siov1.GatewayTLSConfig{
102+
CertificateRefs: []gatewaynetworkingk8siov1.SecretObjectReference{{
103+
Name: "tls-cert",
104+
}},
105+
},
106+
}},
107+
},
108+
}
109+
110+
warnings, err := validator.ValidateCreate(context.Background(), gateway)
111+
require.NoError(t, err)
112+
assert.Empty(t, warnings)
113+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package v1
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/types"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
logf "sigs.k8s.io/controller-runtime/pkg/log"
28+
"sigs.k8s.io/controller-runtime/pkg/webhook"
29+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
30+
gatewaynetworkingk8siov1 "sigs.k8s.io/gateway-api/apis/v1"
31+
32+
internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
33+
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
34+
)
35+
36+
var grpcRouteLog = logf.Log.WithName("grpcroute-resource")
37+
38+
func SetupGRPCRouteWebhookWithManager(mgr ctrl.Manager) error {
39+
return ctrl.NewWebhookManagedBy(mgr).
40+
For(&gatewaynetworkingk8siov1.GRPCRoute{}).
41+
WithValidator(NewGRPCRouteCustomValidator(mgr.GetClient())).
42+
Complete()
43+
}
44+
45+
// +kubebuilder:webhook:path=/validate-gateway-networking-k8s-io-v1-grpcroute,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=create;update,versions=v1,name=vgrpcroute-v1.kb.io,admissionReviewVersions=v1
46+
47+
type GRPCRouteCustomValidator struct {
48+
Client client.Client
49+
checker reference.Checker
50+
}
51+
52+
var _ webhook.CustomValidator = &GRPCRouteCustomValidator{}
53+
54+
func NewGRPCRouteCustomValidator(c client.Client) *GRPCRouteCustomValidator {
55+
return &GRPCRouteCustomValidator{
56+
Client: c,
57+
checker: reference.NewChecker(c, grpcRouteLog),
58+
}
59+
}
60+
61+
func (v *GRPCRouteCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
62+
route, ok := obj.(*gatewaynetworkingk8siov1.GRPCRoute)
63+
if !ok {
64+
return nil, fmt.Errorf("expected a GRPCRoute object but got %T", obj)
65+
}
66+
grpcRouteLog.Info("Validation for GRPCRoute upon creation", "name", route.GetName(), "namespace", route.GetNamespace())
67+
68+
return v.collectWarnings(ctx, route), nil
69+
}
70+
71+
func (v *GRPCRouteCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
72+
route, ok := newObj.(*gatewaynetworkingk8siov1.GRPCRoute)
73+
if !ok {
74+
return nil, fmt.Errorf("expected a GRPCRoute object for the newObj but got %T", newObj)
75+
}
76+
grpcRouteLog.Info("Validation for GRPCRoute upon update", "name", route.GetName(), "namespace", route.GetNamespace())
77+
78+
return v.collectWarnings(ctx, route), nil
79+
}
80+
81+
func (*GRPCRouteCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
82+
return nil, nil
83+
}
84+
85+
func (v *GRPCRouteCustomValidator) collectWarnings(ctx context.Context, route *gatewaynetworkingk8siov1.GRPCRoute) admission.Warnings {
86+
serviceVisited := make(map[types.NamespacedName]struct{})
87+
namespace := route.GetNamespace()
88+
89+
var warnings admission.Warnings
90+
91+
addServiceWarning := func(nn types.NamespacedName) {
92+
if nn.Name == "" || nn.Namespace == "" {
93+
return
94+
}
95+
if _, seen := serviceVisited[nn]; seen {
96+
return
97+
}
98+
serviceVisited[nn] = struct{}{}
99+
warnings = append(warnings, v.checker.Service(ctx, reference.ServiceRef{
100+
Object: route,
101+
NamespacedName: nn,
102+
})...)
103+
}
104+
105+
addBackendRef := func(ns string, name string, group *gatewaynetworkingk8siov1.Group, kind *gatewaynetworkingk8siov1.Kind) {
106+
if name == "" {
107+
return
108+
}
109+
if group != nil && string(*group) != corev1.GroupName {
110+
return
111+
}
112+
if kind != nil && *kind != internaltypes.KindService {
113+
return
114+
}
115+
nn := types.NamespacedName{Namespace: ns, Name: name}
116+
addServiceWarning(nn)
117+
}
118+
119+
processFilters := func(filters []gatewaynetworkingk8siov1.GRPCRouteFilter) {
120+
for _, filter := range filters {
121+
if filter.RequestMirror != nil {
122+
targetNamespace := namespace
123+
if filter.RequestMirror.BackendRef.Namespace != nil && *filter.RequestMirror.BackendRef.Namespace != "" {
124+
targetNamespace = string(*filter.RequestMirror.BackendRef.Namespace)
125+
}
126+
addBackendRef(targetNamespace, string(filter.RequestMirror.BackendRef.Name),
127+
filter.RequestMirror.BackendRef.Group, filter.RequestMirror.BackendRef.Kind)
128+
}
129+
}
130+
}
131+
132+
for _, rule := range route.Spec.Rules {
133+
for _, backend := range rule.BackendRefs {
134+
targetNamespace := namespace
135+
if backend.Namespace != nil && *backend.Namespace != "" {
136+
targetNamespace = string(*backend.Namespace)
137+
}
138+
addBackendRef(targetNamespace, string(backend.Name), backend.Group, backend.Kind)
139+
processFilters(backend.Filters)
140+
}
141+
142+
processFilters(rule.Filters)
143+
}
144+
145+
return warnings
146+
}

0 commit comments

Comments
 (0)