Skip to content

Commit 1475659

Browse files
Add resourcediscovery package to help discover and model Gateway API resource relationships
1 parent f80f447 commit 1475659

File tree

4 files changed

+1223
-0
lines changed

4 files changed

+1223
-0
lines changed
Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
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+
17+
package resourcediscovery
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
25+
"sigs.k8s.io/gateway-api/gwctl/pkg/common"
26+
"sigs.k8s.io/gateway-api/gwctl/pkg/policymanager"
27+
"sigs.k8s.io/gateway-api/gwctl/pkg/relations"
28+
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31+
"k8s.io/apimachinery/pkg/fields"
32+
"k8s.io/apimachinery/pkg/labels"
33+
"k8s.io/apimachinery/pkg/runtime/schema"
34+
apimachinerytypes "k8s.io/apimachinery/pkg/types"
35+
"k8s.io/client-go/discovery"
36+
"k8s.io/klog/v2"
37+
"k8s.io/utils/strings/slices"
38+
)
39+
40+
// Filter struct defines parameters for filtering resources
41+
type Filter struct {
42+
Namespace string
43+
Name string
44+
Labels map[string]string
45+
ResourceType string
46+
}
47+
48+
// Discoverer orchestrates the discovery of resources and their associated
49+
// policies, building a model of interconnected resources.
50+
//
51+
// TODO: Optimization Task: Implement a heuristic within each discovery function
52+
// to intelligently choose between:
53+
// - Single API calls for efficient bulk fetching when appropriate.
54+
// - Multiple API calls for targeted retrieval when necessary.
55+
type Discoverer struct {
56+
K8sClients *common.K8sClients
57+
PolicyManager *policymanager.PolicyManager
58+
}
59+
60+
// DiscoverResourcesForGatewayClass discovers resources related to a
61+
// GatewayClass.
62+
func (d Discoverer) DiscoverResourcesForGatewayClass(filter Filter) (*ResourceModel, error) {
63+
ctx := context.Background()
64+
resourceModel := &ResourceModel{}
65+
66+
gatewayClasses, err := fetchGatewayClasses(ctx, d.K8sClients, filter)
67+
if err != nil {
68+
return resourceModel, err
69+
}
70+
resourceModel.addGatewayClasses(gatewayClasses...)
71+
72+
d.discoverPolicies(ctx, resourceModel)
73+
74+
return resourceModel, nil
75+
}
76+
77+
// DiscoverResourcesForGateway discovers resources related to a Gateway.
78+
func (d Discoverer) DiscoverResourcesForGateway(filter Filter) (*ResourceModel, error) {
79+
ctx := context.Background()
80+
resourceModel := &ResourceModel{}
81+
82+
gateways, err := fetchGateways(ctx, d.K8sClients, filter)
83+
if err != nil {
84+
return resourceModel, err
85+
}
86+
resourceModel.addGateways(gateways...)
87+
88+
d.discoverGatewayClassesFromGateways(ctx, resourceModel)
89+
d.discoverNamespaces(ctx, resourceModel)
90+
d.discoverPolicies(ctx, resourceModel)
91+
92+
resourceModel.calculateEffectivePolicies()
93+
94+
return resourceModel, nil
95+
}
96+
97+
// DiscoverResourcesForHTTPRoute discovers resources related to an HTTPRoute.
98+
func (d Discoverer) DiscoverResourcesForHTTPRoute(filter Filter) (*ResourceModel, error) {
99+
ctx := context.Background()
100+
resourceModel := &ResourceModel{}
101+
102+
httpRoutes, err := fetchHTTPRoutes(ctx, d.K8sClients, filter)
103+
if err != nil {
104+
return resourceModel, err
105+
}
106+
resourceModel.addHTTPRoutes(httpRoutes...)
107+
108+
d.discoverGatewaysFromHTTPRoutes(ctx, resourceModel)
109+
d.discoverGatewayClassesFromGateways(ctx, resourceModel)
110+
d.discoverNamespaces(ctx, resourceModel)
111+
d.discoverPolicies(ctx, resourceModel)
112+
113+
resourceModel.calculateEffectivePolicies()
114+
115+
return resourceModel, nil
116+
}
117+
118+
// DiscoverResourcesForBackend discovers resources related to a Backend.
119+
func (d Discoverer) DiscoverResourcesForBackend(filter Filter) (*ResourceModel, error) {
120+
ctx := context.Background()
121+
resourceModel := &ResourceModel{}
122+
123+
backends, err := fetchBackends(ctx, d.K8sClients, filter)
124+
if err != nil {
125+
return resourceModel, err
126+
}
127+
resourceModel.addBackends(backends...)
128+
129+
d.discoverHTTPRoutesFromBackends(ctx, resourceModel)
130+
d.discoverGatewaysFromHTTPRoutes(ctx, resourceModel)
131+
d.discoverGatewayClassesFromGateways(ctx, resourceModel)
132+
d.discoverNamespaces(ctx, resourceModel)
133+
d.discoverPolicies(ctx, resourceModel)
134+
135+
resourceModel.calculateEffectivePolicies()
136+
137+
return resourceModel, nil
138+
}
139+
140+
// discoverGatewayClassesFromGateways will add GatewayClasses associated with
141+
// Gateways in the resourceModel.
142+
func (d Discoverer) discoverGatewayClassesFromGateways(ctx context.Context, resourceModel *ResourceModel) {
143+
gatewayClasses, err := fetchGatewayClasses(ctx, d.K8sClients, Filter{ /* all GatewayClasses */ })
144+
if err != nil {
145+
klog.V(1).ErrorS(err, "Failed to list all GatewayClasses")
146+
}
147+
148+
// Build temporary index for GatewayClasses
149+
gatewayClassesByID := make(map[gatewayClassID]gatewayv1.GatewayClass)
150+
for _, gatewayClass := range gatewayClasses {
151+
gatewayClassesByID[GatewayClassID(gatewayClass.GetName())] = gatewayClass
152+
}
153+
154+
for gatewayID, gatewayNode := range resourceModel.Gateways {
155+
gatewayClassID := GatewayClassID(relations.FindGatewayClassNameForGateway(*gatewayNode.Gateway))
156+
gatewayClass, ok := gatewayClassesByID[gatewayClassID]
157+
if !ok {
158+
klog.V(1).ErrorS(nil, "GatewayClass referenced in Gateway does not exist",
159+
"gateway", gatewayNode.Gateway.GetNamespace()+"/"+gatewayNode.Gateway.GetName(),
160+
)
161+
continue
162+
}
163+
164+
resourceModel.addGatewayClasses(gatewayClass)
165+
resourceModel.connectGatewayWithGatewayClass(gatewayID, gatewayClassID)
166+
}
167+
}
168+
169+
// discoverGatewaysFromHTTPRoutes will add Gateways associated with HTTPRoutes
170+
// in the resourceModel.
171+
func (d Discoverer) discoverGatewaysFromHTTPRoutes(ctx context.Context, resourceModel *ResourceModel) {
172+
// Visit all gateways corresponding to the httpRoutes
173+
for _, httpRouteNode := range resourceModel.HTTPRoutes {
174+
for _, gatewayRef := range relations.FindGatewayRefsForHTTPRoute(*httpRouteNode.HTTPRoute) {
175+
// Check if Gateway already exists in the resourceModel.
176+
if _, ok := resourceModel.Gateways[GatewayID(gatewayRef.Namespace, gatewayRef.Name)]; ok {
177+
// Gateway already exists in the resourceModel, skip re-fetching.
178+
continue
179+
}
180+
181+
// Gateway doesn't already exist so fetch and add it to the resourceModel.
182+
gateways, err := fetchGateways(ctx, d.K8sClients, Filter{Namespace: gatewayRef.Namespace, Name: gatewayRef.Name})
183+
if err != nil {
184+
klog.V(1).ErrorS(err, "Gateway referenced by HTTPRoute not found",
185+
"gateway", gatewayRef.String(),
186+
"httproute", httpRouteNode.HTTPRoute.GetNamespace()+"/"+httpRouteNode.HTTPRoute.GetName(),
187+
)
188+
continue
189+
}
190+
resourceModel.addGateways(gateways[0])
191+
}
192+
}
193+
194+
// Connect gatewayd with httproutes.
195+
for httpRouteID, httpRouteNode := range resourceModel.HTTPRoutes {
196+
for _, gatewayRef := range relations.FindGatewayRefsForHTTPRoute(*httpRouteNode.HTTPRoute) {
197+
resourceModel.connectHTTPRouteWithGateway(httpRouteID, GatewayID(gatewayRef.Namespace, gatewayRef.Name))
198+
}
199+
}
200+
}
201+
202+
// discoverHTTPRoutesFromBackends will add HTTPRoutes that reference any Backend
203+
// present in resourceModel.
204+
func (d Discoverer) discoverHTTPRoutesFromBackends(ctx context.Context, resourceModel *ResourceModel) {
205+
httpRoutes, err := fetchHTTPRoutes(ctx, d.K8sClients, Filter{ /* all HTTPRoutes */ })
206+
if err != nil {
207+
klog.V(1).ErrorS(err, "Failed to list all HTTPRoutes")
208+
}
209+
210+
for _, httpRoute := range httpRoutes {
211+
var found bool
212+
for _, backendRef := range relations.FindBackendRefsForHTTPRoute(httpRoute) {
213+
backendID := BackendID(backendRef.Group, backendRef.Kind, backendRef.Namespace, backendRef.Name)
214+
_, ok := resourceModel.Backends[backendID]
215+
if !ok {
216+
continue
217+
}
218+
found = true
219+
220+
resourceModel.addHTTPRoutes(httpRoute)
221+
resourceModel.connectHTTPRouteWithBackend(HTTPRouteID(httpRoute.GetNamespace(), httpRoute.GetName()), backendID)
222+
}
223+
if !found {
224+
klog.V(1).InfoS("Skipping HTTPRoute since it does not reference any required Backend",
225+
"httpRoute", httpRoute.GetNamespace()+"/"+httpRoute.GetName(),
226+
)
227+
}
228+
}
229+
}
230+
231+
// discoverNamespaces discovers Namespaces for resources that exist in the
232+
// resourceModel.
233+
func (d Discoverer) discoverNamespaces(ctx context.Context, resourceModel *ResourceModel) {
234+
for gatewayID, gatewayNode := range resourceModel.Gateways {
235+
resourceModel.addNamespace(gatewayNode.Gateway.GetNamespace())
236+
resourceModel.connectGatewayWithNamespace(gatewayID, NamespaceID(gatewayNode.Gateway.GetNamespace()))
237+
}
238+
for httpRouteID, httpRouteNode := range resourceModel.HTTPRoutes {
239+
resourceModel.addNamespace(httpRouteNode.HTTPRoute.GetNamespace())
240+
resourceModel.connectHTTPRouteWithNamespace(httpRouteID, NamespaceID(httpRouteNode.HTTPRoute.GetNamespace()))
241+
}
242+
for backendID, backendNode := range resourceModel.Backends {
243+
resourceModel.addNamespace(backendNode.Backend.GetNamespace())
244+
resourceModel.connectBackendWithNamespace(backendID, NamespaceID(backendNode.Backend.GetNamespace()))
245+
}
246+
}
247+
248+
// discoverPolicies discovers Policies for resources that exist in the
249+
// resourceModel.
250+
func (d Discoverer) discoverPolicies(ctx context.Context, resourceModel *ResourceModel) {
251+
resourceModel.addPolicyIfTargetExists(d.PolicyManager.GetPolicies()...)
252+
}
253+
254+
// fetchGatewayClasses fetches GatewayClasses based on a filter.
255+
func fetchGatewayClasses(ctx context.Context, k8sClients *common.K8sClients, filter Filter) ([]gatewayv1.GatewayClass, error) {
256+
if filter.Name != "" {
257+
// Use Get call.
258+
gatewayClass := &gatewayv1.GatewayClass{}
259+
nn := apimachinerytypes.NamespacedName{Name: filter.Name}
260+
if err := k8sClients.Client.Get(ctx, nn, gatewayClass); err != nil {
261+
return []gatewayv1.GatewayClass{}, err
262+
}
263+
264+
return []gatewayv1.GatewayClass{*gatewayClass}, nil
265+
}
266+
267+
// Use List call.
268+
options := &client.ListOptions{
269+
Namespace: filter.Namespace,
270+
LabelSelector: labels.SelectorFromSet(filter.Labels),
271+
}
272+
gatewayClassList := &gatewayv1.GatewayClassList{}
273+
if err := k8sClients.Client.List(ctx, gatewayClassList, options); err != nil {
274+
return []gatewayv1.GatewayClass{}, err
275+
}
276+
277+
return gatewayClassList.Items, nil
278+
}
279+
280+
// fetchGateways fetches Gateways based on a filter.
281+
func fetchGateways(ctx context.Context, k8sClients *common.K8sClients, filter Filter) ([]gatewayv1.Gateway, error) {
282+
if filter.Name != "" {
283+
// Use Get call.
284+
gateway := &gatewayv1.Gateway{}
285+
nn := apimachinerytypes.NamespacedName{Namespace: filter.Namespace, Name: filter.Name}
286+
if err := k8sClients.Client.Get(ctx, nn, gateway); err != nil {
287+
return []gatewayv1.Gateway{}, err
288+
}
289+
290+
return []gatewayv1.Gateway{*gateway}, nil
291+
}
292+
293+
// Use List call.
294+
options := &client.ListOptions{
295+
Namespace: filter.Namespace,
296+
LabelSelector: labels.SelectorFromSet(filter.Labels),
297+
}
298+
gatewayList := &gatewayv1.GatewayList{}
299+
if err := k8sClients.Client.List(ctx, gatewayList, options); err != nil {
300+
return []gatewayv1.Gateway{}, err
301+
}
302+
303+
return gatewayList.Items, nil
304+
}
305+
306+
// fetchHTTPRoutes fetches HTTPRoutes based on a filter.
307+
func fetchHTTPRoutes(ctx context.Context, k8sClients *common.K8sClients, filter Filter) ([]gatewayv1.HTTPRoute, error) {
308+
if filter.Name != "" {
309+
// Use Get call.
310+
httpRoute := &gatewayv1.HTTPRoute{}
311+
nn := apimachinerytypes.NamespacedName{Namespace: filter.Namespace, Name: filter.Name}
312+
if err := k8sClients.Client.Get(ctx, nn, httpRoute); err != nil {
313+
return []gatewayv1.HTTPRoute{}, err
314+
}
315+
316+
return []gatewayv1.HTTPRoute{*httpRoute}, nil
317+
}
318+
319+
// Use List call.
320+
options := &client.ListOptions{
321+
Namespace: filter.Namespace,
322+
LabelSelector: labels.SelectorFromSet(filter.Labels),
323+
}
324+
httpRouteList := &gatewayv1.HTTPRouteList{}
325+
if err := k8sClients.Client.List(ctx, httpRouteList, options); err != nil {
326+
return []gatewayv1.HTTPRoute{}, err
327+
}
328+
329+
return httpRouteList.Items, nil
330+
}
331+
332+
// fetchBackends fetches Backends based on a filter.
333+
func fetchBackends(ctx context.Context, k8sClients *common.K8sClients, filter Filter) ([]unstructured.Unstructured, error) {
334+
apiResource, err := apiResourceFromResourceType(filter.ResourceType, k8sClients.DiscoveryClient)
335+
if err != nil {
336+
return nil, err
337+
}
338+
gvr := schema.GroupVersionResource{
339+
Group: apiResource.Group,
340+
Version: apiResource.Version,
341+
Resource: apiResource.Name,
342+
}
343+
344+
listOptions := metav1.ListOptions{}
345+
if filter.Name != "" {
346+
listOptions.FieldSelector = fields.OneTermEqualSelector("metadata.name", filter.Name).String()
347+
}
348+
349+
var backendsList *unstructured.UnstructuredList
350+
if apiResource.Namespaced {
351+
backendsList, err = k8sClients.DC.Resource(gvr).Namespace(filter.Namespace).List(ctx, listOptions)
352+
} else {
353+
backendsList, err = k8sClients.DC.Resource(gvr).List(ctx, listOptions)
354+
}
355+
if err != nil {
356+
return nil, err
357+
}
358+
359+
return backendsList.Items, nil
360+
}
361+
362+
func apiResourceFromResourceType(resourceType string, discoveryClient discovery.DiscoveryInterface) (metav1.APIResource, error) {
363+
resourceGroups, err := discoveryClient.ServerPreferredResources()
364+
if err != nil {
365+
return metav1.APIResource{}, err
366+
}
367+
for _, resourceGroup := range resourceGroups {
368+
gv, err := schema.ParseGroupVersion(resourceGroup.GroupVersion)
369+
if err != nil {
370+
return metav1.APIResource{}, err
371+
}
372+
for _, resource := range resourceGroup.APIResources {
373+
var choices []string
374+
choices = append(choices, resource.Kind)
375+
choices = append(choices, resource.Name)
376+
choices = append(choices, resource.ShortNames...)
377+
choices = append(choices, resource.SingularName)
378+
if slices.Contains(choices, resourceType) {
379+
resource.Version = gv.Version
380+
return resource, nil
381+
}
382+
}
383+
}
384+
return metav1.APIResource{}, fmt.Errorf("GVR for %v not found in discovery", resourceType)
385+
}

0 commit comments

Comments
 (0)