diff --git a/gwctl/cmd/describe.go b/gwctl/cmd/describe.go index 474dd6fa26..8c45a8bee8 100644 --- a/gwctl/cmd/describe.go +++ b/gwctl/cmd/describe.go @@ -122,7 +122,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "httproute", "httproutes": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{ @@ -142,7 +142,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "gateway", "gateways": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{ @@ -162,7 +162,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "gatewayclass", "gatewayclasses": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{ @@ -181,7 +181,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "backend", "backends": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{ @@ -201,7 +201,7 @@ func runDescribe(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "namespace", "namespaces", "ns": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{ diff --git a/gwctl/cmd/get.go b/gwctl/cmd/get.go index 5787aa734d..432b57985d 100644 --- a/gwctl/cmd/get.go +++ b/gwctl/cmd/get.go @@ -104,7 +104,7 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "namespace", "namespaces", "ns": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } resourceModel, err = discoverer.DiscoverResourcesForNamespace(resourcediscovery.Filter{Labels: selector}) @@ -117,7 +117,7 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "gateway", "gateways": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{Namespace: ns, Labels: selector} @@ -134,7 +134,7 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "gatewayclass", "gatewayclasses": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{Namespace: ns, Labels: selector} @@ -161,7 +161,7 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "httproute", "httproutes": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{Namespace: ns, Labels: selector} @@ -178,7 +178,7 @@ func runGet(cmd *cobra.Command, args []string, params *utils.CmdParams) { case "backend", "backends": selector, err := labels.Parse(labelSelector) if err != nil { - fmt.Fprintf(os.Stderr, "Unable to find resources that match the label selector \"%s\": %v\n", labelSelector, err) + fmt.Fprintf(os.Stderr, "Failed to parse label selector %q: %v\n", labelSelector, err) os.Exit(1) } filter := resourcediscovery.Filter{Namespace: ns, Labels: selector} diff --git a/gwctl/pkg/common/testhelpers.go b/gwctl/pkg/common/testhelpers.go index 46bf354bff..4abc8d8d34 100644 --- a/gwctl/pkg/common/testhelpers.go +++ b/gwctl/pkg/common/testhelpers.go @@ -22,6 +22,8 @@ import ( "strings" "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // YamlString defines a custom type for wrapping yaml texts. It makes use of @@ -71,3 +73,14 @@ func (src JSONString) CmpDiff(tgt JSONString) (diff string, err error) { return cmp.Diff(srcMap, targetMap), nil } + +func NamespaceForTest(name string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Status: corev1.NamespaceStatus{ + Phase: corev1.NamespaceActive, + }, + } +} diff --git a/gwctl/pkg/common/types.go b/gwctl/pkg/common/types.go new file mode 100644 index 0000000000..a62352c0ee --- /dev/null +++ b/gwctl/pkg/common/types.go @@ -0,0 +1,26 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +// ObjRef defines a reference to a Kubernetes resource, using plain strings for +// easier comparison. +type ObjRef struct { + Group string `json:",omitempty"` + Kind string `json:",omitempty"` + Name string `json:",omitempty"` + Namespace string `json:",omitempty"` +} diff --git a/gwctl/pkg/printer/backends_test.go b/gwctl/pkg/printer/backends_test.go index f885065108..7ac29c3383 100644 --- a/gwctl/pkg/printer/backends_test.go +++ b/gwctl/pkg/printer/backends_test.go @@ -21,20 +21,17 @@ import ( "testing" "time" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/ptr" - - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" testingclock "k8s.io/utils/clock/testing" + "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/resourcediscovery" "sigs.k8s.io/gateway-api/gwctl/pkg/utils" @@ -43,181 +40,64 @@ import ( func TestBackendsPrinter_Print(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) - healthCheckPolicies := []runtime.Object{ - &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Name: "healthcheckpolicies.foo.com", - Labels: map[string]string{ - gatewayv1alpha2.PolicyLabelKey: "inherited", - }, - }, - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.ClusterScoped, - Group: "foo.com", - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}}, - Names: apiextensionsv1.CustomResourceDefinitionNames{ - Plural: "healthcheckpolicies", - Kind: "HealthCheckPolicy", - }, - }, - }, - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-gatewayclass", - "creationTimestamp": fakeClock.Now().Add(-6 * 24 * time.Hour).Format(time.RFC3339), - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "value-parent-1", - "key3": "value-parent-3", - "key5": "value-parent-5", - }, - "default": map[string]interface{}{ - "key2": "value-parent-2", - "key4": "value-parent-4", - }, - "targetRef": map[string]interface{}{ - "group": "", - "kind": "Service", - "name": "foo-svc-0", - "namespace": "default", - }, - }, - }, - }, - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "foo.com/v1", - "kind": "HealthCheckPolicy", - "metadata": map[string]interface{}{ - "name": "health-check-gateway", - "creationTimestamp": fakeClock.Now().Add(-20 * 24 * time.Hour).Format(time.RFC3339), - }, - "spec": map[string]interface{}{ - "override": map[string]interface{}{ - "key1": "value-child-1", - }, - "default": map[string]interface{}{ - "key2": "value-child-2", - "key5": "value-child-5", - }, - "targetRef": map[string]interface{}{ - "group": "", - "kind": "Service", - "name": "foo-svc-1", - "namespace": "ns1", - }, - }, - }, - }, - } - - timeoutPolicies := []runtime.Object{ - &apiextensionsv1.CustomResourceDefinition{ + httpRoute := func(namespace, name, serviceName, gatewayName string) *gatewayv1.HTTPRoute { + return &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "timeoutpolicies.bar.com", - Labels: map[string]string{ - gatewayv1alpha2.PolicyLabelKey: "direct", - }, - }, - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Scope: apiextensionsv1.ClusterScoped, - Group: "bar.com", - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}}, - Names: apiextensionsv1.CustomResourceDefinitionNames{ - Plural: "timeoutpolicies", - Kind: "TimeoutPolicy", + Name: name, + Namespace: namespace, + CreationTimestamp: metav1.Time{ + Time: fakeClock.Now().Add(-24 * time.Hour), }, }, - }, - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-namespace", - "creationTimestamp": fakeClock.Now().Add(-5 * time.Minute).Format(time.RFC3339), - }, - "spec": map[string]interface{}{ - "condition": "path=/abc", - "seconds": int64(30), - "targetRef": map[string]interface{}{ - "kind": "Namespace", - "name": "default", + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{ + { + Kind: common.PtrTo(gatewayv1.Kind("Gateway")), + Group: common.PtrTo(gatewayv1.Group("gateway.networking.k8s.io")), + Namespace: common.PtrTo(gatewayv1.Namespace(namespace)), + Name: gatewayv1.ObjectName(gatewayName), + }, }, }, - }, - }, - &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "bar.com/v1", - "kind": "TimeoutPolicy", - "metadata": map[string]interface{}{ - "name": "timeout-policy-httproute", - "creationTimestamp": fakeClock.Now().Add(-13 * time.Minute).Format(time.RFC3339), - }, - "spec": map[string]interface{}{ - "condition": "path=/def", - "seconds": int64(60), - "targetRef": map[string]interface{}{ - "group": "gateway.networking.k8s.io", - "kind": "HTTPRoute", - "name": "bar-route-21", - "namespace": "ns1", + Rules: []gatewayv1.HTTPRouteRule{ + { + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Port: ptr.To(gatewayv1.PortNumber(8080)), + Name: gatewayv1.ObjectName(serviceName), + Kind: ptr.To(gatewayv1.Kind("Service")), + Namespace: ptr.To(gatewayv1.Namespace(namespace)), + }, + }, + }, + }, }, }, }, - }, + } } objects := []runtime.Object{ + common.NamespaceForTest("ns1"), &gatewayv1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{ - Name: "demo-gatewayclass-1", + Name: "foo-gatewayclass-1", }, Spec: gatewayv1.GatewayClassSpec{ ControllerName: "example.net/gateway-controller", Description: common.PtrTo("random"), }, }, - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ns1", - }, - Status: corev1.NamespaceStatus{ - Phase: corev1.NamespaceActive, - }, - }, - &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ns2", - }, - Status: corev1.NamespaceStatus{ - Phase: corev1.NamespaceActive, - }, - }, - &corev1.Namespace{ + &gatewayv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ - Name: "ns3", - }, - Status: corev1.NamespaceStatus{ - Phase: corev1.NamespaceActive, - }, - }, - &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", + Name: "foo-gateway-1", + Namespace: "ns1", }, - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-svc-0", - Namespace: "default", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-72 * time.Hour), - }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "foo-gatewayclass-1", }, }, &corev1.Service{ @@ -229,7 +109,7 @@ func TestBackendsPrinter_Print(t *testing.T) { Name: "foo-svc-1", Namespace: "ns1", CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-48 * time.Hour), + Time: fakeClock.Now().Add(-72 * time.Hour), }, }, }, @@ -240,239 +120,64 @@ func TestBackendsPrinter_Print(t *testing.T) { }, ObjectMeta: metav1.ObjectMeta{ Name: "foo-svc-2", - Namespace: "ns2", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-36 * time.Hour), - }, - }, - }, - &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-svc-3", - Namespace: "ns3", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-24 * time.Hour), - }, - }, - }, - &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-svc-4", - Namespace: "ns3", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-128 * time.Hour), - }, - }, - }, - &gatewayv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "demo-gateway-1", - Namespace: "default", - }, - Spec: gatewayv1.GatewaySpec{ - GatewayClassName: "demo-gatewayclass-1", - }, - }, - &gatewayv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "demo-gateway-2", - Namespace: "ns2", - }, - Spec: gatewayv1.GatewaySpec{ - GatewayClassName: "demo-gatewayclass-1", - }, - }, - &gatewayv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "demo-gateway-345", Namespace: "ns1", - }, - Spec: gatewayv1.GatewaySpec{ - GatewayClassName: "demo-gatewayclass-1", - }, - }, - &gatewayv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo-httproute-1", - Namespace: "default", CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-24 * time.Hour), - }, - }, - Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"example.com", "example2.com", "example3.com"}, - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ - { - Kind: common.PtrTo(gatewayv1.Kind("Gateway")), - Group: common.PtrTo(gatewayv1.Group("gateway.networking.k8s.io")), - Namespace: common.PtrTo(gatewayv1.Namespace("ns2")), - Name: "demo-gateway-2", - }, - }, - }, - Rules: []gatewayv1.HTTPRouteRule{ - { - BackendRefs: []gatewayv1.HTTPBackendRef{ - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8080)), - Name: "foo-svc-0", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("default")), - }, - }, - }, - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8081)), - Name: "foo-svc-1", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns1")), - }, - }, - }, - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8082)), - Name: "foo-svc-2", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns2")), - }, - }, - }, - }, - }, + Time: fakeClock.Now().Add(-48 * time.Hour), }, }, }, - &gatewayv1.HTTPRoute{ + httpRoute("ns1", "foo-httproute-1", "foo-svc-1", "foo-gateway-1"), + httpRoute("ns1", "foo-httproute-2", "foo-svc-2", "foo-gateway-1"), + httpRoute("ns1", "foo-httproute-3", "foo-svc-2", "foo-gateway-1"), + httpRoute("ns1", "foo-httproute-4", "foo-svc-2", "foo-gateway-1"), + httpRoute("ns1", "foo-httproute-5", "foo-svc-2", "foo-gateway-1"), + } + + backendPolicies := []runtime.Object{ + &apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ - Name: "qmn-httproute-100", - Namespace: "default", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-11 * time.Hour), + Name: "healthcheckpolicies.foo.com", + Labels: map[string]string{ + gatewayv1alpha2.PolicyLabelKey: "Direct", }, }, - Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"example.com"}, - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ - { - Kind: common.PtrTo(gatewayv1.Kind("Gateway")), - Group: common.PtrTo(gatewayv1.Group("gateway.networking.k8s.io")), - Name: "demo-gateway-1", - }, - { - Kind: common.PtrTo(gatewayv1.Kind("Gateway")), - Group: common.PtrTo(gatewayv1.Group("gateway.networking.k8s.io")), - Name: "demo-gateway-345", - }, - }, - }, - Rules: []gatewayv1.HTTPRouteRule{ - { - BackendRefs: []gatewayv1.HTTPBackendRef{ - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8081)), - Name: "foo-svc-1", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns1")), - }, - }, - }, - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8082)), - Name: "foo-svc-2", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns2")), - }, - }, - }, - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8083)), - Name: "foo-svc-3", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns3")), - }, - }, - }, - }, - }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Scope: apiextensionsv1.NamespaceScoped, + Group: "foo.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{Name: "v1"}}, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "healthcheckpolicies", + Kind: "HealthCheckPolicy", }, }, }, - &gatewayv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "bar-route-21", - Namespace: "ns1", - CreationTimestamp: metav1.Time{ - Time: fakeClock.Now().Add(-9 * time.Hour), + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "foo.com/v1", + "kind": "HealthCheckPolicy", + "metadata": map[string]interface{}{ + "name": "health-check-gatewayclass", + "namespace": "default", + "creationTimestamp": fakeClock.Now().Add(-6 * 24 * time.Hour).Format(time.RFC3339), }, - }, - Spec: gatewayv1.HTTPRouteSpec{ - Hostnames: []gatewayv1.Hostname{"foo.com", "bar.com", "example.com", "example2.com", "example3.com", "example4.com", "example5.com"}, - CommonRouteSpec: gatewayv1.CommonRouteSpec{ - ParentRefs: []gatewayv1.ParentReference{ - { - Kind: common.PtrTo(gatewayv1.Kind("Gateway")), - Group: common.PtrTo(gatewayv1.Group("gateway.networking.k8s.io")), - Namespace: common.PtrTo(gatewayv1.Namespace("default")), - Name: "demo-gateway-2", - }, + "spec": map[string]interface{}{ + "default": map[string]interface{}{ + "key2": "value-parent-2", }, - }, - Rules: []gatewayv1.HTTPRouteRule{ - { - BackendRefs: []gatewayv1.HTTPBackendRef{ - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8082)), - Name: "foo-svc-2", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns2")), - }, - }, - }, - { - BackendRef: gatewayv1.BackendRef{ - BackendObjectReference: gatewayv1.BackendObjectReference{ - Port: ptr.To(gatewayv1.PortNumber(8083)), - Name: "foo-svc-3", - Kind: ptr.To(gatewayv1.Kind("Service")), - Namespace: ptr.To(gatewayv1.Namespace("ns3")), - }, - }, - }, - }, + "targetRef": map[string]interface{}{ + "group": "", + "kind": "Service", + "name": "foo-svc-1", + "namespace": "ns1", }, }, }, }, } - finalObjects := []runtime.Object{} - finalObjects = append(finalObjects, healthCheckPolicies...) - finalObjects = append(finalObjects, timeoutPolicies...) + var finalObjects []runtime.Object finalObjects = append(finalObjects, objects...) + finalObjects = append(finalObjects, backendPolicies...) params := utils.MustParamsForTest(t, common.MustClientsForTest(t, finalObjects...)) discoverer := resourcediscovery.Discoverer{ @@ -493,12 +198,9 @@ func TestBackendsPrinter_Print(t *testing.T) { got := params.Out.(*bytes.Buffer).String() want := ` -NAMESPACE NAME TYPE REFERRED BY ROUTES AGE POLICIES -default foo-svc-0 Service default/foo-httproute-1 3d 1 -ns1 foo-svc-1 Service default/foo-httproute-1,default/qmn-httproute-100 2d 1 -ns2 foo-svc-2 Service default/foo-httproute-1,default/qmn-httproute-100 + 1 more 36h 0 -ns3 foo-svc-3 Service default/qmn-httproute-100,ns1/bar-route-21 24h 0 -ns3 foo-svc-4 Service None 5d8h 0 +NAMESPACE NAME TYPE REFERRED BY ROUTES AGE POLICIES +ns1 foo-svc-1 Service ns1/foo-httproute-1 3d 1 +ns1 foo-svc-2 Service ns1/foo-httproute-2,ns1/foo-httproute-3 + 2 more 2d 0 ` if diff := cmp.Diff(common.YamlString(want), common.YamlString(got), common.YamlStringTransformer); diff != "" { t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", got, want, diff) diff --git a/gwctl/pkg/relations/relations.go b/gwctl/pkg/relations/relations.go index 13e120c10c..133d6dafb2 100644 --- a/gwctl/pkg/relations/relations.go +++ b/gwctl/pkg/relations/relations.go @@ -20,20 +20,13 @@ package relations import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/gwctl/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" ) -// ObjRef defines a reference to a Kubernetes resource, using plain strings for -// easier comparison. -type ObjRef struct { - Group string `json:",omitempty"` - Kind string `json:",omitempty"` - Name string `json:",omitempty"` - Namespace string `json:",omitempty"` -} - // FindGatewayRefsForHTTPRoute returns Gateways which the HTTPRoute is attached // to. func FindGatewayRefsForHTTPRoute(httpRoute gatewayv1.HTTPRoute) []types.NamespacedName { @@ -61,7 +54,7 @@ func FindGatewayClassNameForGateway(gateway gatewayv1.Gateway) string { } // FindBackendRefsForHTTPRoute returns Backends which the HTTPRoute references. -func FindBackendRefsForHTTPRoute(httpRoute gatewayv1.HTTPRoute) []ObjRef { +func FindBackendRefsForHTTPRoute(httpRoute gatewayv1.HTTPRoute) []common.ObjRef { // Aggregate all BackendRefs var backendRefs []gatewayv1.BackendObjectReference for _, rule := range httpRoute.Spec.Rules { @@ -81,9 +74,9 @@ func FindBackendRefsForHTTPRoute(httpRoute gatewayv1.HTTPRoute) []ObjRef { // Convert each BackendRef to ObjRef. ObjRef does not use pointers and thus is // easily comparable. - resultSet := make(map[ObjRef]bool) + resultSet := make(map[common.ObjRef]bool) for _, backendRef := range backendRefs { - objRef := ObjRef{ + objRef := common.ObjRef{ Name: string(backendRef.Name), // Assume namespace is unspecified in the backendRef and check later to // override the default value. @@ -102,9 +95,48 @@ func FindBackendRefsForHTTPRoute(httpRoute gatewayv1.HTTPRoute) []ObjRef { } // Return unique objRefs - var result []ObjRef + var result []common.ObjRef for objRef := range resultSet { result = append(result, objRef) } return result } + +// ReferenceGrantExposes returns true if the provided reference grant "exposes" +// the given resource. "Exposes" means that the resource is part of the "To" +// fields within the ReferenceGrant. +func ReferenceGrantExposes(referenceGrant gatewayv1beta1.ReferenceGrant, resource common.ObjRef) bool { + if referenceGrant.GetNamespace() != resource.Namespace { + return false + } + for _, to := range referenceGrant.Spec.To { + if to.Group != gatewayv1.Group(resource.Group) { + continue + } + if to.Kind != gatewayv1.Kind(resource.Kind) { + continue + } + if to.Name == nil || len(*to.Name) == 0 || *to.Name == gatewayv1.ObjectName(resource.Name) { + return true + } + } + return false +} + +// ReferenceGrantAccepts returns true if the provided reference grant "accepts" +// references from the given resource. "Accepts" means that the resource is part +// of the "From" fields within the ReferenceGrant. +func ReferenceGrantAccepts(referenceGrant gatewayv1beta1.ReferenceGrant, resource common.ObjRef) bool { + resource.Name = "" + for _, from := range referenceGrant.Spec.From { + fromRef := common.ObjRef{ + Group: string(from.Group), + Kind: string(from.Kind), + Namespace: string(from.Namespace), + } + if fromRef == resource { + return true + } + } + return false +} diff --git a/gwctl/pkg/resourcediscovery/discoverer.go b/gwctl/pkg/resourcediscovery/discoverer.go index 3c97822e90..9850099672 100644 --- a/gwctl/pkg/resourcediscovery/discoverer.go +++ b/gwctl/pkg/resourcediscovery/discoverer.go @@ -24,11 +24,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" "sigs.k8s.io/gateway-api/gwctl/pkg/relations" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" @@ -46,9 +48,10 @@ const ( ) var ( - defaultGatewayClassGroupVersion = gatewayv1.GroupVersion - defaultGatewayGroupVersion = gatewayv1.GroupVersion - defaultHTTPRouteGroupVersion = gatewayv1.GroupVersion + defaultGatewayClassGroupVersion = gatewayv1.GroupVersion + defaultGatewayGroupVersion = gatewayv1.GroupVersion + defaultHTTPRouteGroupVersion = gatewayv1.GroupVersion + defaultReferenceGrantGroupVersion = gatewayv1beta1.GroupVersion ) // Filter struct defines parameters for filtering resources @@ -73,9 +76,10 @@ type Discoverer struct { // attempt will be made to discover this information from the discovery APIs. // Failure to do so will mean we use the "default" versions defined in this // file. - PreferredGatewayClassGroupVersion metav1.GroupVersion - PreferredGatewayGroupVersion metav1.GroupVersion - PreferredHTTPRouteGroupVersion metav1.GroupVersion + PreferredGatewayClassGroupVersion metav1.GroupVersion + PreferredGatewayGroupVersion metav1.GroupVersion + PreferredHTTPRouteGroupVersion metav1.GroupVersion + PreferredReferenceGrantGroupVersion metav1.GroupVersion } func NewDiscoverer(k8sClients *common.K8sClients, policyManager *policymanager.PolicyManager) Discoverer { @@ -201,6 +205,7 @@ func (d Discoverer) DiscoverResourcesForBackend(filter Filter) (*ResourceModel, } resourceModel.addBackends(backends...) + d.discoverReferenceGrantsFromBackends(ctx, resourceModel) d.discoverHTTPRoutesFromBackends(ctx, resourceModel) d.discoverGatewaysFromHTTPRoutes(ctx, resourceModel) d.discoverGatewayClassesFromGateways(ctx, resourceModel) @@ -246,12 +251,16 @@ func (d Discoverer) discoverGatewayClassesFromGateways(ctx context.Context, reso } for gatewayID, gatewayNode := range resourceModel.Gateways { - gwcID := GatewayClassID(relations.FindGatewayClassNameForGateway(*gatewayNode.Gateway)) + gatewayClassName := relations.FindGatewayClassNameForGateway(*gatewayNode.Gateway) + gwcID := GatewayClassID(gatewayClassName) gatewayClass, ok := gatewayClassesByID[gwcID] if !ok { - klog.V(1).ErrorS(nil, "GatewayClass referenced in Gateway does not exist", - "gateway", gatewayNode.Gateway.GetNamespace()+"/"+gatewayNode.Gateway.GetName(), - ) + err := ReferenceToNonExistentResourceError{ReferenceFromTo: ReferenceFromTo{ + ReferringObject: common.ObjRef{Kind: "Gateway", Name: gatewayNode.Gateway.GetName(), Namespace: gatewayNode.Gateway.GetNamespace()}, + ReferredObject: common.ObjRef{Kind: "GatewayClass", Name: gatewayClassName}, + }} + gatewayNode.Errors = append(gatewayNode.Errors, err) + klog.V(1).Info(err) continue } @@ -275,10 +284,19 @@ func (d Discoverer) discoverGatewaysFromHTTPRoutes(ctx context.Context, resource // Gateway doesn't already exist so fetch and add it to the resourceModel. gateways, err := d.fetchGateways(ctx, Filter{Namespace: gatewayRef.Namespace, Name: gatewayRef.Name, Labels: labels.Everything()}) if err != nil { - klog.V(1).ErrorS(err, "Gateway referenced by HTTPRoute not found", - "gateway", gatewayRef.String(), - "httproute", httpRouteNode.HTTPRoute.GetNamespace()+"/"+httpRouteNode.HTTPRoute.GetName(), - ) + if apierrors.IsNotFound(err) { + err := ReferenceToNonExistentResourceError{ReferenceFromTo: ReferenceFromTo{ + ReferringObject: common.ObjRef{Kind: "HTTPRoute", Name: httpRouteNode.HTTPRoute.GetName(), Namespace: httpRouteNode.HTTPRoute.GetNamespace()}, + ReferredObject: common.ObjRef{Kind: "Gateway", Name: gatewayRef.Name, Namespace: gatewayRef.Namespace}, + }} + httpRouteNode.Errors = append(httpRouteNode.Errors, err) + klog.V(1).Info(err) + } else { + klog.V(1).ErrorS(err, "Error while fetching Gateway for HTTPRoute", + "gateway", gatewayRef.String(), + "httproute", httpRouteNode.HTTPRoute.GetNamespace()+"/"+httpRouteNode.HTTPRoute.GetName(), + ) + } continue } resourceModel.addGateways(gateways[0]) @@ -346,19 +364,56 @@ func (d Discoverer) discoverHTTPRoutesFromBackends(ctx context.Context, resource } for _, httpRoute := range httpRoutes { - var found bool + // An HTTPRoute will be included in the resourceModel if it references some + // Backend which already exists in the resourceModel. + var includeRouteInResourceModel bool + for _, backendRef := range relations.FindBackendRefsForHTTPRoute(httpRoute) { + // Check if the referenced backend exists in the resourceModel. backendID := BackendID(backendRef.Group, backendRef.Kind, backendRef.Namespace, backendRef.Name) - _, ok := resourceModel.Backends[backendID] + backendNode, ok := resourceModel.Backends[backendID] if !ok { continue } - found = true + + // Ensure that if this is a cross namespace reference, then it is accepted + // through some ReferenceGrant. + if httpRoute.GetNamespace() != backendRef.Namespace { + httpRouteRef := common.ObjRef{ + Group: httpRoute.GroupVersionKind().Group, + Kind: httpRoute.GroupVersionKind().Kind, + Name: httpRoute.GetName(), + Namespace: httpRoute.GetNamespace(), + } + var referenceAccepted bool + for _, referenceGrantNode := range backendNode.ReferenceGrants { + if relations.ReferenceGrantAccepts(*referenceGrantNode.ReferenceGrant, httpRouteRef) { + referenceAccepted = true + break + } + } + if !referenceAccepted { + err := ReferenceNotPermittedError{ReferenceFromTo: ReferenceFromTo{ + ReferringObject: common.ObjRef{Kind: "HTTPRoute", Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()}, + ReferredObject: backendRef, + }} + backendNode.Errors = append(backendNode.Errors, err) + klog.V(1).Info(err) + continue + } + } + + // At this point, we know that: + // - The HTTPRoute references some backend which exists in the resourceModel. + // - The referenced backend is either in the same namespace as the + // HTTPRoute, or is exposed through a ReferenceGrant. + includeRouteInResourceModel = true resourceModel.addHTTPRoutes(httpRoute) resourceModel.connectHTTPRouteWithBackend(HTTPRouteID(httpRoute.GetNamespace(), httpRoute.GetName()), backendID) } - if !found { + + if !includeRouteInResourceModel { klog.V(1).InfoS("Skipping HTTPRoute since it does not reference any required Backend", "httpRoute", httpRoute.GetNamespace()+"/"+httpRoute.GetName(), ) @@ -394,6 +449,40 @@ func (d Discoverer) discoverNamespaces(ctx context.Context, resourceModel *Resou } } +func (d Discoverer) discoverReferenceGrantsFromBackends(ctx context.Context, resourceModel *ResourceModel) { + referenceGrantsByNamespace := make(map[string][]gatewayv1beta1.ReferenceGrant) + for _, backendNode := range resourceModel.Backends { + backendNS := backendNode.Backend.GetNamespace() + + referenceGrants, ok := referenceGrantsByNamespace[backendNS] + if !ok { + var err error + referenceGrants, err = d.fetchReferenceGrants(ctx, Filter{Namespace: backendNS, Labels: labels.Everything()}) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to fetch list of ReferenceGrants: %v\n", err) + os.Exit(1) + } + } + + for _, referenceGrant := range referenceGrants { + backendRef := common.ObjRef{ + Group: backendNode.Backend.GroupVersionKind().Group, + Kind: backendNode.Backend.GroupVersionKind().Kind, + Name: backendNode.Backend.GetName(), + Namespace: backendNode.Backend.GetNamespace(), + } + if relations.ReferenceGrantExposes(referenceGrant, backendRef) { + klog.V(1).InfoS("ReferenceGrant exposes Backend", + "referenceGrant", referenceGrant.GetNamespace()+"/"+referenceGrant.GetName(), + "backendRef", backendRef.Namespace+"/"+backendRef.Name, + ) + resourceModel.addReferenceGrants(referenceGrant) + resourceModel.connectReferenceGrantWithBackend(ReferenceGrantID(referenceGrant.GetNamespace(), referenceGrant.GetName()), backendNode.ID()) + } + } + } +} + // discoverPolicies adds Policies for resources that exist in the resourceModel. func (d Discoverer) discoverPolicies(resourceModel *ResourceModel) { resourceModel.addPolicyIfTargetExists(d.PolicyManager.GetPolicies()...) @@ -552,6 +641,45 @@ func (d Discoverer) fetchHTTPRoutes(ctx context.Context, filter Filter) ([]gatew return httpRouteList.Items, nil } +// fetchHTTPRoutes fetches HTTPRoutes based on a filter. +func (d Discoverer) fetchReferenceGrants(ctx context.Context, filter Filter) ([]gatewayv1beta1.ReferenceGrant, error) { + gvr := schema.GroupVersionResource{ + Group: defaultReferenceGrantGroupVersion.Group, + Version: defaultReferenceGrantGroupVersion.Version, + Resource: "referencegrants", + } + if d.PreferredReferenceGrantGroupVersion != (metav1.GroupVersion{}) { + gvr.Version = d.PreferredReferenceGrantGroupVersion.Version + } + + if filter.Name != "" { + // Use Get call. + referenceGrantUnstructured, err := d.K8sClients.DC.Resource(gvr).Namespace(filter.Namespace).Get(ctx, filter.Name, metav1.GetOptions{}) + if err != nil { + return []gatewayv1beta1.ReferenceGrant{}, err + } + referenceGrant := &gatewayv1beta1.ReferenceGrant{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(referenceGrantUnstructured.UnstructuredContent(), referenceGrant); err != nil { + return []gatewayv1beta1.ReferenceGrant{}, fmt.Errorf("failed to convert unstructured ReferenceGrant to structured: %v", err) + } + return []gatewayv1beta1.ReferenceGrant{*referenceGrant}, nil + } + + // Use List call. + listOptions := metav1.ListOptions{ + LabelSelector: filter.Labels.String(), + } + referenceGrantListUnstructured, err := d.K8sClients.DC.Resource(gvr).Namespace(filter.Namespace).List(ctx, listOptions) + if err != nil { + return []gatewayv1beta1.ReferenceGrant{}, err + } + referenceGrantList := &gatewayv1beta1.ReferenceGrantList{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(referenceGrantListUnstructured.UnstructuredContent(), referenceGrantList); err != nil { + return []gatewayv1beta1.ReferenceGrant{}, fmt.Errorf("failed to convert unstructured ReferenceGrantList to structured: %v", err) + } + return referenceGrantList.Items, nil +} + // fetchBackends fetches Backends based on a filter. // // At the moment, this is exclusively used for Backends of type Service, though diff --git a/gwctl/pkg/resourcediscovery/discoverer_test.go b/gwctl/pkg/resourcediscovery/discoverer_test.go index 69675382b6..3b8157b9f1 100644 --- a/gwctl/pkg/resourcediscovery/discoverer_test.go +++ b/gwctl/pkg/resourcediscovery/discoverer_test.go @@ -21,17 +21,324 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + apimachinerytypes "k8s.io/apimachinery/pkg/types" testingclock "k8s.io/utils/clock/testing" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/gateway-api/gwctl/pkg/common" "sigs.k8s.io/gateway-api/gwctl/pkg/utils" ) +func TestDiscoverResourcesForGateway(t *testing.T) { + testcases := []struct { + name string + objects []runtime.Object + filter Filter + + wantGateways []apimachinerytypes.NamespacedName + // wantGatewayErrors maps a Gateway to the list of errors that Gateway has. + wantGatewayErrors map[apimachinerytypes.NamespacedName][]error + }{ + { + name: "normal", + filter: Filter{Labels: labels.Everything()}, + objects: []runtime.Object{ + common.NamespaceForTest("default"), + &gatewayv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gatewayclass", + }, + }, + &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gateway", + Namespace: "default", + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "foo-gatewayclass", + }, + }, + }, + wantGateways: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-gateway"}, + }, + wantGatewayErrors: map[apimachinerytypes.NamespacedName][]error{ + {Namespace: "default", Name: "foo-gateway"}: nil, // Want no errors. + }, + }, + { + name: "gateway should have error if it references a non-existent gatewayclass", + filter: Filter{Labels: labels.Everything()}, + objects: []runtime.Object{ + common.NamespaceForTest("default"), + &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-gateway", + Namespace: "default", + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: "foo-gatewayclass", // GatewayClass does not exist. + }, + }, + }, + wantGateways: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-gateway"}, + }, + wantGatewayErrors: map[apimachinerytypes.NamespacedName][]error{ + {Namespace: "default", Name: "foo-gateway"}: { + ReferenceToNonExistentResourceError{ReferenceFromTo: ReferenceFromTo{ + ReferringObject: common.ObjRef{Kind: "Gateway", Name: "foo-gateway", Namespace: "default"}, + ReferredObject: common.ObjRef{Kind: "GatewayClass", Name: "foo-gatewayclass"}, + }}, + }, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, tc.objects...)) + discoverer := Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + + resourceModel, err := discoverer.DiscoverResourcesForGateway(tc.filter) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + gotGateways := namespacedGatewaysFromResourceModel(resourceModel) + gotGatewayErrors := gatewayErrorsFromResourceModel(resourceModel) + + if tc.wantGateways != nil { + if diff := cmp.Diff(tc.wantGateways, gotGateways, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff in Gateways; got=%v, want=%v;\ndiff (-want +got)=\n%v", gotGateways, tc.wantGateways, diff) + } + } + if tc.wantGatewayErrors != nil { + if diff := cmp.Diff(tc.wantGatewayErrors, gotGatewayErrors, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff in Gateway errors; got=%v, want=%v;\ndiff (-want +got)=\n%v", gotGatewayErrors, tc.wantGatewayErrors, diff) + } + } + }) + } +} + +func TestDiscoverResourcesForBackend(t *testing.T) { + testcases := []struct { + name string + objects []runtime.Object + filter Filter + + wantBackends []apimachinerytypes.NamespacedName + wantHTTPRoutes []apimachinerytypes.NamespacedName + }{ + { + name: "normal", + filter: Filter{Labels: labels.Everything()}, + objects: []runtime.Object{ + common.NamespaceForTest("default"), + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc", + Namespace: "default", + }, + }, + &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-httproute", + Namespace: "default", + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{}, + Rules: []gatewayv1.HTTPRouteRule{ + { + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Kind: common.PtrTo(gatewayv1.Kind("Service")), + Name: "foo-svc", + Port: common.PtrTo(gatewayv1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantBackends: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-svc"}, + }, + wantHTTPRoutes: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-httproute"}, + }, + }, + { + name: "httproute from different namespace should require referencegrant", + filter: Filter{Labels: labels.Everything()}, + objects: []runtime.Object{ + common.NamespaceForTest("default"), + common.NamespaceForTest("bar"), + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc", + Namespace: "default", + }, + }, + &gatewayv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bar-httproute", + Namespace: "bar", // Different namespace than Service. + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{}, + Rules: []gatewayv1.HTTPRouteRule{ + { + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Kind: common.PtrTo(gatewayv1.Kind("Service")), + Name: "foo-svc", + Namespace: common.PtrTo(gatewayv1.Namespace("default")), + Port: common.PtrTo(gatewayv1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantBackends: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-svc"}, + }, + wantHTTPRoutes: []apimachinerytypes.NamespacedName{}, + }, + { + name: "httproute from different namespace should get allowed with referencegrant", + filter: Filter{Labels: labels.Everything()}, + objects: []runtime.Object{ + common.NamespaceForTest("default"), + common.NamespaceForTest("bar"), + &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-svc", + Namespace: "default", + }, + }, + &gatewayv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gatewayv1.GroupVersion.String(), + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "bar-httproute", + Namespace: "bar", // Different namespace than Service. + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{}, + Rules: []gatewayv1.HTTPRouteRule{ + { + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Kind: common.PtrTo(gatewayv1.Kind("Service")), + Name: "foo-svc", + Namespace: common.PtrTo(gatewayv1.Namespace("default")), + Port: common.PtrTo(gatewayv1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + }, + &gatewayv1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-reference-grant", + Namespace: "default", + }, + Spec: gatewayv1beta1.ReferenceGrantSpec{ + From: []gatewayv1beta1.ReferenceGrantFrom{{ + Group: gatewayv1.Group(gatewayv1.GroupVersion.Group), + Kind: "HTTPRoute", + Namespace: "bar", + }}, + To: []gatewayv1beta1.ReferenceGrantTo{{ + Kind: "Service", + }}, + }, + }, + }, + wantBackends: []apimachinerytypes.NamespacedName{ + {Namespace: "default", Name: "foo-svc"}, + }, + wantHTTPRoutes: []apimachinerytypes.NamespacedName{ + {Namespace: "bar", Name: "bar-httproute"}, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + params := utils.MustParamsForTest(t, common.MustClientsForTest(t, tc.objects...)) + discoverer := Discoverer{ + K8sClients: params.K8sClients, + PolicyManager: params.PolicyManager, + } + + resourceModel, err := discoverer.DiscoverResourcesForBackend(tc.filter) + if err != nil { + t.Fatalf("Failed to construct resourceModel: %v", resourceModel) + } + + gotBackends := namespacedBackendsFromResourceModel(resourceModel) + gotHTTPRoutes := namespacedHTTPRoutesFromResourceModel(resourceModel) + + if tc.wantBackends != nil { + if diff := cmp.Diff(tc.wantBackends, gotBackends, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff in Backends; got=%v, want=%v;\ndiff (-want +got)=\n%v", gotBackends, tc.wantBackends, diff) + } + } + if tc.wantHTTPRoutes != nil { + if diff := cmp.Diff(tc.wantHTTPRoutes, gotHTTPRoutes, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("Unexpected diff in HTTPRoutes; got=%v, want=%v;\ndiff (-want +got)=\n%v", gotHTTPRoutes, tc.wantHTTPRoutes, diff) + } + } + }) + } +} + // TestDiscoverResourcesForGatewayClass_LabelSelector Tests label selector filtering for GatewayClasses. func TestDiscoverResourcesForGatewayClass_LabelSelector(t *testing.T) { fakeClock := testingclock.NewFakeClock(time.Now()) @@ -290,3 +597,48 @@ func TestDiscoverResourcesForNamespace_LabelSelector(t *testing.T) { t.Errorf("Unexpected diff\ngot=\n%v\nwant=\n%v\ndiff (-want +got)=\n%v", expectedNamespaceNames, namespaceNames, diff) } } + +func namespacedGatewaysFromResourceModel(r *ResourceModel) []apimachinerytypes.NamespacedName { + var gateways []apimachinerytypes.NamespacedName + for _, gatewayNode := range r.Gateways { + gateways = append(gateways, apimachinerytypes.NamespacedName{ + Namespace: gatewayNode.Gateway.GetNamespace(), + Name: gatewayNode.Gateway.GetName(), + }) + } + return gateways +} + +func namespacedHTTPRoutesFromResourceModel(r *ResourceModel) []apimachinerytypes.NamespacedName { + var httpRoutes []apimachinerytypes.NamespacedName + for _, httpRouteNode := range r.HTTPRoutes { + httpRoutes = append(httpRoutes, apimachinerytypes.NamespacedName{ + Namespace: httpRouteNode.HTTPRoute.GetNamespace(), + Name: httpRouteNode.HTTPRoute.GetName(), + }) + } + return httpRoutes +} + +func namespacedBackendsFromResourceModel(r *ResourceModel) []apimachinerytypes.NamespacedName { + var backends []apimachinerytypes.NamespacedName + for _, backendNode := range r.Backends { + backends = append(backends, apimachinerytypes.NamespacedName{ + Namespace: backendNode.Backend.GetNamespace(), + Name: backendNode.Backend.GetName(), + }) + } + return backends +} + +func gatewayErrorsFromResourceModel(r *ResourceModel) map[apimachinerytypes.NamespacedName][]error { + result := make(map[apimachinerytypes.NamespacedName][]error) + for _, gatewayNode := range r.Gateways { + gatewayNN := apimachinerytypes.NamespacedName{ + Namespace: gatewayNode.Gateway.GetNamespace(), + Name: gatewayNode.Gateway.GetName(), + } + result[gatewayNN] = gatewayNode.Errors + } + return result +} diff --git a/gwctl/pkg/resourcediscovery/errors.go b/gwctl/pkg/resourcediscovery/errors.go new file mode 100644 index 0000000000..5ad9034b55 --- /dev/null +++ b/gwctl/pkg/resourcediscovery/errors.go @@ -0,0 +1,84 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcediscovery + +import ( + "fmt" + + "sigs.k8s.io/gateway-api/gwctl/pkg/common" +) + +type ReferenceToNonExistentResourceError struct { + ReferenceFromTo +} + +func (r ReferenceToNonExistentResourceError) Error() string { + return fmt.Sprintf("%v %q references a non-existent %v %q", + r.referringObjectKind(), r.referringObjectName(), + r.referredObjectKind(), r.referredObjectName()) +} + +type ReferenceNotPermittedError struct { + ReferenceFromTo +} + +func (r ReferenceNotPermittedError) Error() string { + return fmt.Sprintf("%v %q is not permitted to reference %v %q", + r.referringObjectKind(), r.referringObjectName(), + r.referredObjectKind(), r.referredObjectName()) +} + +type ReferenceFromTo struct { + // ReferringObject is the "from" object which is referring "to" some other + // object. + ReferringObject common.ObjRef + // ReferredObject is the actual object which is being referenced by another + // object. + ReferredObject common.ObjRef +} + +// referringObjectKind returns a human readable Kind. +func (r ReferenceFromTo) referringObjectKind() string { + if r.ReferringObject.Group != "" { + return fmt.Sprintf("%v(.%v)", r.ReferringObject.Kind, r.ReferringObject.Group) + } + return r.ReferringObject.Kind +} + +// referredObjectKind returns a human readable Kind. +func (r ReferenceFromTo) referredObjectKind() string { + if r.ReferredObject.Group != "" { + return fmt.Sprintf("%v(.%v)", r.ReferredObject.Kind, r.ReferredObject.Group) + } + return r.ReferredObject.Kind +} + +// referringObjectName returns a human readable Name. +func (r ReferenceFromTo) referringObjectName() string { + if r.ReferringObject.Namespace != "" { + return fmt.Sprintf("%v/%v", r.ReferringObject.Namespace, r.ReferringObject.Name) + } + return r.ReferringObject.Name +} + +// referredObjectName returns a human readable Name. +func (r ReferenceFromTo) referredObjectName() string { + if r.ReferredObject.Namespace != "" { + return fmt.Sprintf("%v/%v", r.ReferredObject.Namespace, r.ReferredObject.Name) + } + return r.ReferredObject.Name +} diff --git a/gwctl/pkg/resourcediscovery/main_test.go b/gwctl/pkg/resourcediscovery/main_test.go new file mode 100644 index 0000000000..3cd8fb6e17 --- /dev/null +++ b/gwctl/pkg/resourcediscovery/main_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcediscovery + +import ( + "flag" + "os" + "testing" + + "k8s.io/klog/v2" +) + +func TestMain(m *testing.M) { + fs := flag.NewFlagSet("mock-flags", flag.PanicOnError) + klog.InitFlags(fs) + fs.Set("v", "3") // Set klog verbosity. + + os.Exit(m.Run()) +} diff --git a/gwctl/pkg/resourcediscovery/nodes.go b/gwctl/pkg/resourcediscovery/nodes.go index e496e945f9..ab0bb092df 100644 --- a/gwctl/pkg/resourcediscovery/nodes.go +++ b/gwctl/pkg/resourcediscovery/nodes.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" corev1 "k8s.io/api/core/v1" @@ -44,12 +45,13 @@ func (r resourceID) String() string { } type ( - gatewayClassID resourceID - namespaceID resourceID - gatewayID resourceID - httpRouteID resourceID - backendID resourceID - policyID resourceID + gatewayClassID resourceID + namespaceID resourceID + gatewayID resourceID + httpRouteID resourceID + backendID resourceID + referenceGrantID resourceID + policyID resourceID ) // GatewayClassID returns an ID for a GatewayClass. @@ -107,6 +109,14 @@ func PolicyID(group, kind, namespace, name string) policyID { //nolint:revive }) } +// ReferenceGrantID returns an ID for a ReferenceGrant. +func ReferenceGrantID(namespace, name string) referenceGrantID { //nolint:revive + return referenceGrantID(resourceID{ + Namespace: namespace, + Name: name, + }) +} + // MarshalText is used to implement encoding.TextMarshaler interface for // gatewayID. func (g gatewayID) MarshalText() ([]byte, error) { @@ -161,6 +171,8 @@ type GatewayNode struct { EffectivePolicies map[policymanager.PolicyCrdID]policymanager.Policy // Events contains the events associated with this Gateway. Events []corev1.Event + // Errors contains any errorrs associated with this resource. + Errors []error } func NewGatewayNode(gateway *gatewayv1.Gateway) *GatewayNode { @@ -170,6 +182,7 @@ func NewGatewayNode(gateway *gatewayv1.Gateway) *GatewayNode { Policies: make(map[policyID]*PolicyNode), EffectivePolicies: make(map[policymanager.PolicyCrdID]policymanager.Policy), Events: []corev1.Event{}, + Errors: []error{}, } } @@ -201,6 +214,8 @@ type HTTPRouteNode struct { // EffectivePolicies reflects the effective policies applicable to this // HTTPRoute, mapped per Gateway for context-specific enforcement. EffectivePolicies map[gatewayID]map[policymanager.PolicyCrdID]policymanager.Policy + // Errors contains any errorrs associated with this resource. + Errors []error } func NewHTTPRouteNode(httpRoute *gatewayv1.HTTPRoute) *HTTPRouteNode { @@ -210,6 +225,7 @@ func NewHTTPRouteNode(httpRoute *gatewayv1.HTTPRoute) *HTTPRouteNode { Backends: make(map[backendID]*BackendNode), Policies: make(map[policyID]*PolicyNode), EffectivePolicies: make(map[gatewayID]map[policymanager.PolicyCrdID]policymanager.Policy), + Errors: []error{}, } } @@ -237,9 +253,13 @@ type BackendNode struct { HTTPRoutes map[httpRouteID]*HTTPRouteNode // Policies stores Policies directly applied to the Backend. Policies map[policyID]*PolicyNode + // ReferenceGrants contains ReferenceGrants that expose this Backend. + ReferenceGrants map[referenceGrantID]*ReferenceGrantNode // EffectivePolicies reflects the effective policies applicable to this // Backend, mapped per Gateway for context-specific enforcement. EffectivePolicies map[gatewayID]map[policymanager.PolicyCrdID]policymanager.Policy + // Errors contains any errorrs associated with this resource. + Errors []error } func NewBackendNode(backend *unstructured.Unstructured) *BackendNode { @@ -247,7 +267,9 @@ func NewBackendNode(backend *unstructured.Unstructured) *BackendNode { Backend: backend, HTTPRoutes: make(map[httpRouteID]*HTTPRouteNode), Policies: make(map[policyID]*PolicyNode), + ReferenceGrants: make(map[referenceGrantID]*ReferenceGrantNode), EffectivePolicies: make(map[gatewayID]map[policymanager.PolicyCrdID]policymanager.Policy), + Errors: []error{}, } } @@ -304,6 +326,30 @@ func (n *NamespaceNode) ID() namespaceID { //nolint:revive return NamespaceID(n.Namespace.Name) } +// ReferenceGrantNode models the relationships and dependencies of a ReferenceGrant. +type ReferenceGrantNode struct { + // ReferenceGrantName identifies the ReferenceGrant. + ReferenceGrant *gatewayv1beta1.ReferenceGrant + + // Backends lists Backends residing within the ReferenceGrant. + Backends map[backendID]*BackendNode +} + +func NewReferenceGrantNode(referenceGrant *gatewayv1beta1.ReferenceGrant) *ReferenceGrantNode { + return &ReferenceGrantNode{ + ReferenceGrant: referenceGrant, + Backends: make(map[backendID]*BackendNode), + } +} + +func (r *ReferenceGrantNode) ID() referenceGrantID { //nolint:revive + if r.ReferenceGrant.Name == "" { + klog.V(0).ErrorS(nil, "returning empty ID since ReferenceGrant is empty") + return referenceGrantID{} + } + return ReferenceGrantID(r.ReferenceGrant.GetNamespace(), r.ReferenceGrant.GetName()) +} + // PolicyNode models the relationships and dependencies of a Policy resource type PolicyNode struct { // Policy references the actual Policy resource. diff --git a/gwctl/pkg/resourcediscovery/resourcemodel.go b/gwctl/pkg/resourcediscovery/resourcemodel.go index 99be5c632f..9096a02f7f 100644 --- a/gwctl/pkg/resourcediscovery/resourcemodel.go +++ b/gwctl/pkg/resourcediscovery/resourcemodel.go @@ -21,6 +21,7 @@ import ( "sort" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "sigs.k8s.io/gateway-api/gwctl/pkg/policymanager" corev1 "k8s.io/api/core/v1" @@ -38,12 +39,13 @@ import ( // - Identifying potential conflicts or issues in resource configuration // - Visualizing the topology of Gateway API resources type ResourceModel struct { - GatewayClasses map[gatewayClassID]*GatewayClassNode - Namespaces map[namespaceID]*NamespaceNode - Gateways map[gatewayID]*GatewayNode - HTTPRoutes map[httpRouteID]*HTTPRouteNode - Backends map[backendID]*BackendNode - Policies map[policyID]*PolicyNode + GatewayClasses map[gatewayClassID]*GatewayClassNode + Namespaces map[namespaceID]*NamespaceNode + Gateways map[gatewayID]*GatewayNode + HTTPRoutes map[httpRouteID]*HTTPRouteNode + Backends map[backendID]*BackendNode + ReferenceGrants map[referenceGrantID]*ReferenceGrantNode + Policies map[policyID]*PolicyNode } // addGatewayClasses adds nodes for GatewayClases. @@ -115,6 +117,20 @@ func (rm *ResourceModel) addBackends(backends ...unstructured.Unstructured) { } } +// addReferenceGrants adds nodes for ReferenceGrants. +func (rm *ResourceModel) addReferenceGrants(referenceGrants ...gatewayv1beta1.ReferenceGrant) { + if rm.ReferenceGrants == nil { + rm.ReferenceGrants = make(map[referenceGrantID]*ReferenceGrantNode) + } + for _, referenceGrant := range referenceGrants { + referenceGrant := referenceGrant + referenceGrantNode := NewReferenceGrantNode(&referenceGrant) + if _, ok := rm.ReferenceGrants[referenceGrantNode.ID()]; !ok { + rm.ReferenceGrants[referenceGrantNode.ID()] = referenceGrantNode + } + } +} + // addPolicyIfTargetExists adds a node for Policy only if the target for the // Policy exists in the ResourceModel. In addition to adding the Node, it also // makes the connections with the targetRefs. @@ -296,6 +312,24 @@ func (rm *ResourceModel) connectBackendWithNamespace(backendID backendID, namesp namespaceNode.Backends[backendID] = backendNode } +// connectReferenceGrantWithBackend establishes a connection between a ReferenceGrant and +// a Backend. +func (rm *ResourceModel) connectReferenceGrantWithBackend(referenceGrantID referenceGrantID, backendID backendID) { + referenceGrantNode, ok := rm.ReferenceGrants[referenceGrantID] + if !ok { + klog.V(1).ErrorS(nil, "ReferenceGrant does not exist in ResourceModel", "referenceGrantID", referenceGrantID) + return + } + backendNode, ok := rm.Backends[backendID] + if !ok { + klog.V(1).ErrorS(nil, "Backend does not exist in ResourceModel", "backendID", backendID) + return + } + + referenceGrantNode.Backends[backendID] = backendNode + backendNode.ReferenceGrants[referenceGrantID] = referenceGrantNode +} + // calculateEffectivePolicies calculates the effective policies for all // Gateways, HTTPRoutes, and Backends in the ResourceModel. func (rm *ResourceModel) calculateEffectivePolicies() error { @@ -316,6 +350,13 @@ func (rm *ResourceModel) calculateEffectivePolicies() error { // Namespace, and Gateway). func (rm *ResourceModel) calculateEffectivePoliciesForGateways() error { for _, gatewayNode := range rm.Gateways { + // Do not calculate effective policy for the Gateway if the referenced + // GatewayClass does not exist. For now, we only calculate effective policy + // once the references are corrected. + if gatewayNode.GatewayClass == nil { + continue + } + // Fetch all policies. gatewayClassPolicies := convertPoliciesMapToSlice(gatewayNode.GatewayClass.Policies) gatewayNamespacePolicies := convertPoliciesMapToSlice(gatewayNode.Namespace.Policies)