diff --git a/conformance/tests/gateway-observed-generation-bump.go b/conformance/tests/gateway-observed-generation-bump.go new file mode 100644 index 0000000000..d7b5d87bda --- /dev/null +++ b/conformance/tests/gateway-observed-generation-bump.go @@ -0,0 +1,88 @@ +/* +Copyright 2022 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 tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, GatewayObservedGenerationBump) +} + +var GatewayObservedGenerationBump = suite.ConformanceTest{ + ShortName: "GatewayObservedGenerationBump", + Description: "A Gateway in the gateway-conformance-infra namespace should update the observedGeneration in all of it's Status.Conditions after an update to the spec", + Manifests: []string{"tests/gateway-observed-generation-bump.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + gwNN := types.NamespacedName{Name: "gateway-observed-generation-bump", Namespace: "gateway-conformance-infra"} + + t.Run("observedGeneration should increment", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + namespaces := []string{"gateway-conformance-infra"} + kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces) + + original := &v1beta1.Gateway{} + err := s.Client.Get(ctx, gwNN, original) + require.NoErrorf(t, err, "error getting Gateway: %v", err) + + // Sanity check + kubernetes.GatewayMustHaveLatestConditions(t, original) + + all := v1beta1.NamespacesFromAll + + mutate := original.DeepCopy() + + // mutate the Gateway Spec + mutate.Spec.Listeners = append(mutate.Spec.Listeners, v1beta1.Listener{ + Name: "alternate", + Port: 8080, + Protocol: v1beta1.HTTPProtocolType, + AllowedRoutes: &v1beta1.AllowedRoutes{ + Namespaces: &v1beta1.RouteNamespaces{From: &all}, + }, + }) + + err = s.Client.Update(ctx, mutate) + require.NoErrorf(t, err, "error updating the Gateway: %v", err) + + // Ensure the generation and observedGeneration sync up + kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces) + + updated := &v1beta1.Gateway{} + err = s.Client.Get(ctx, gwNN, updated) + require.NoErrorf(t, err, "error getting Gateway: %v", err) + + // Sanity check + kubernetes.GatewayMustHaveLatestConditions(t, updated) + + require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update") + }) + }, +} diff --git a/conformance/tests/gateway-observed-generation-bump.yaml b/conformance/tests/gateway-observed-generation-bump.yaml new file mode 100644 index 0000000000..31fe593d9f --- /dev/null +++ b/conformance/tests/gateway-observed-generation-bump.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway-observed-generation-bump + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: All diff --git a/conformance/tests/gatewayclass-observed-generation-bump.go b/conformance/tests/gatewayclass-observed-generation-bump.go new file mode 100644 index 0000000000..0bb08799ff --- /dev/null +++ b/conformance/tests/gatewayclass-observed-generation-bump.go @@ -0,0 +1,77 @@ +/* +Copyright 2022 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 tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, GatewayClassObservedGenerationBump) +} + +var GatewayClassObservedGenerationBump = suite.ConformanceTest{ + ShortName: "GatewayClassObservedGenerationBump", + Features: []suite.SupportedFeature{suite.SupportGatewayClassObservedGenerationBump}, + Description: "A GatewayClass should update the observedGeneration in all of it's Status.Conditions after an update to the spec", + Manifests: []string{"tests/gatewayclass-observed-generation-bump.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + gwc := types.NamespacedName{Name: "gatewayclass-observed-generation-bump"} + + t.Run("observedGeneration should increment", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + kubernetes.GWCMustBeAccepted(t, s.Client, s.TimeoutConfig, gwc.Name) + + original := &v1beta1.GatewayClass{} + err := s.Client.Get(ctx, gwc, original) + require.NoErrorf(t, err, "error getting GatewayClass: %v", err) + + // Sanity check + kubernetes.GatewayClassMustHaveLatestConditions(t, original) + + mutate := original.DeepCopy() + desc := "new" + mutate.Spec.Description = &desc + + err = s.Client.Update(ctx, mutate) + require.NoErrorf(t, err, "error updating the GatewayClass: %v", err) + + // Ensure the generation and observedGeneration sync up + kubernetes.GWCMustBeAccepted(t, s.Client, s.TimeoutConfig, gwc.Name) + + updated := &v1beta1.GatewayClass{} + err = s.Client.Get(ctx, gwc, updated) + require.NoErrorf(t, err, "error getting GatewayClass: %v", err) + + // Sanity check + kubernetes.GatewayClassMustHaveLatestConditions(t, updated) + + require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update") + }) + }, +} diff --git a/conformance/tests/gatewayclass-observed-generation-bump.yaml b/conformance/tests/gatewayclass-observed-generation-bump.yaml new file mode 100644 index 0000000000..0ad3d1fd36 --- /dev/null +++ b/conformance/tests/gatewayclass-observed-generation-bump.yaml @@ -0,0 +1,7 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: gatewayclass-observed-generation-bump +spec: + controllerName: "{GATEWAY_CONTROLLER_NAME}" + description: "old" diff --git a/conformance/tests/httproute-observed-generation-bump.go b/conformance/tests/httproute-observed-generation-bump.go new file mode 100644 index 0000000000..590dcc6fed --- /dev/null +++ b/conformance/tests/httproute-observed-generation-bump.go @@ -0,0 +1,83 @@ +/* +Copyright 2022 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 tests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "sigs.k8s.io/gateway-api/apis/v1beta1" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, HTTPRouteObservedGenerationBump) +} + +var HTTPRouteObservedGenerationBump = suite.ConformanceTest{ + ShortName: "HTTPRouteObservedGenerationBump", + Description: "A HTTPRoute in the gateway-conformance-infra namespace should update the observedGeneration in all of it's Status.Conditions after an update to the spec", + Manifests: []string{"tests/httproute-observed-generation-bump.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + routeNN := types.NamespacedName{Name: "observed-generation-bump", Namespace: "gateway-conformance-infra"} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: "gateway-conformance-infra"} + + acceptedCondition := metav1.Condition{ + Type: string(v1beta1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + Reason: "", // any reason + } + + t.Run("observedGeneration should increment", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + namespaces := []string{"gateway-conformance-infra"} + kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces) + + original := &v1beta1.HTTPRoute{} + err := s.Client.Get(ctx, routeNN, original) + require.NoErrorf(t, err, "error getting HTTPRoute: %v", err) + + // Sanity check + kubernetes.HTTPRouteMustHaveLatestConditions(t, original) + + mutate := original.DeepCopy() + mutate.Spec.Rules[0].BackendRefs[0].Name = "infra-backend-v2" + err = s.Client.Update(ctx, mutate) + require.NoErrorf(t, err, "error updating the HTTPRoute: %v", err) + + kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, acceptedCondition) + + updated := &v1beta1.HTTPRoute{} + err = s.Client.Get(ctx, routeNN, updated) + require.NoErrorf(t, err, "error getting Gateway: %v", err) + + // Sanity check + kubernetes.HTTPRouteMustHaveLatestConditions(t, updated) + + require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update") + }) + }, +} diff --git a/conformance/tests/httproute-observed-generation-bump.yaml b/conformance/tests/httproute-observed-generation-bump.yaml new file mode 100644 index 0000000000..8de5c76455 --- /dev/null +++ b/conformance/tests/httproute-observed-generation-bump.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: observed-generation-bump + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 diff --git a/conformance/utils/kubernetes/apply.go b/conformance/utils/kubernetes/apply.go index 9ff3f4b5ad..da1f5bfe6f 100644 --- a/conformance/utils/kubernetes/apply.go +++ b/conformance/utils/kubernetes/apply.go @@ -49,25 +49,31 @@ type Applier struct { // four ValidUniqueListenerPorts. // If empty or nil, ports are not modified. ValidUniqueListenerPorts []v1beta1.PortNumber + + // GatewayClass will be used as the spec.gatewayClassName when applying Gateway resources + GatewayClass string + + // ControllerName will be used as the spec.controllerName when applying GatewayClass resources + ControllerName string } // prepareGateway adjusts both listener ports and the gatewayClassName. It // returns an index pointing to the next valid listener port. -func prepareGateway(t *testing.T, uObj *unstructured.Unstructured, gatewayClassName string, validListenerPorts []v1beta1.PortNumber, portIndex int) int { - err := unstructured.SetNestedField(uObj.Object, gatewayClassName, "spec", "gatewayClassName") +func (a Applier) prepareGateway(t *testing.T, uObj *unstructured.Unstructured, portIndex int) int { + err := unstructured.SetNestedField(uObj.Object, a.GatewayClass, "spec", "gatewayClassName") require.NoErrorf(t, err, "error setting `spec.gatewayClassName` on %s Gateway resource", uObj.GetName()) - if len(validListenerPorts) > 0 { + if len(a.ValidUniqueListenerPorts) > 0 { listeners, _, err := unstructured.NestedSlice(uObj.Object, "spec", "listeners") require.NoErrorf(t, err, "error getting `spec.listeners` on %s Gateway resource", uObj.GetName()) for i, uListener := range listeners { - require.Less(t, portIndex, len(validListenerPorts), "not enough unassigned valid ports for `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName()) + require.Less(t, portIndex, len(a.ValidUniqueListenerPorts), "not enough unassigned valid ports for `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName()) listener, ok := uListener.(map[string]interface{}) require.Truef(t, ok, "unexpected type at `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName()) - nextPort := validListenerPorts[portIndex] + nextPort := a.ValidUniqueListenerPorts[portIndex] err = unstructured.SetNestedField(listener, int64(nextPort), "port") require.NoErrorf(t, err, "error setting `spec.listeners[%d].port` on %s Gateway resource", i, uObj.GetName()) @@ -82,6 +88,12 @@ func prepareGateway(t *testing.T, uObj *unstructured.Unstructured, gatewayClassN return portIndex } +// prepareGatewayClass adjust the spec.controllerName on the resource +func (a Applier) prepareGatewayClass(t *testing.T, uObj *unstructured.Unstructured) { + err := unstructured.SetNestedField(uObj.Object, a.ControllerName, "spec", "controllerName") + require.NoErrorf(t, err, "error setting `spec.controllerName` on %s GatewayClass resource", uObj.GetName()) +} + // prepareNamespace adjusts the Namespace labels. func prepareNamespace(t *testing.T, uObj *unstructured.Unstructured, namespaceLabels map[string]string) { labels, _, err := unstructured.NestedStringMap(uObj.Object, "metadata", "labels") @@ -104,7 +116,7 @@ func prepareNamespace(t *testing.T, uObj *unstructured.Unstructured, namespaceLa // prepareResources uses the options from an Applier to tweak resources given by // a set of manifests. -func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder, gcName string) ([]unstructured.Unstructured, error) { +func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder) ([]unstructured.Unstructured, error) { var resources []unstructured.Unstructured // portIndex is incremented for each listener we see. For a manifest file @@ -123,8 +135,11 @@ func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder, continue } + if uObj.GetKind() == "GatewayClass" { + a.prepareGatewayClass(t, &uObj) + } if uObj.GetKind() == "Gateway" { - portIndex = prepareGateway(t, &uObj, gcName, a.ValidUniqueListenerPorts, portIndex) + portIndex = a.prepareGateway(t, &uObj, portIndex) } if uObj.GetKind() == "Namespace" && uObj.GetObjectKind().GroupVersionKind().Group == "" { @@ -168,13 +183,13 @@ func (a Applier) MustApplyObjectsWithCleanup(t *testing.T, c client.Client, time // MustApplyWithCleanup creates or updates Kubernetes resources defined with the // provided YAML file and registers a cleanup function for resources it created. // Note that this does not remove resources that already existed in the cluster. -func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, gcName string, cleanup bool) { +func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, cleanup bool) { data, err := getContentsFromPathOrURL(location, timeoutConfig) require.NoError(t, err) decoder := yaml.NewYAMLOrJSONDecoder(data, 4096) - resources, err := a.prepareResources(t, decoder, gcName) + resources, err := a.prepareResources(t, decoder) if err != nil { t.Logf("manifest: %s", data.String()) require.NoErrorf(t, err, "error parsing manifest") diff --git a/conformance/utils/kubernetes/apply_test.go b/conformance/utils/kubernetes/apply_test.go index 3ee201fbe1..70f2593623 100644 --- a/conformance/utils/kubernetes/apply_test.go +++ b/conformance/utils/kubernetes/apply_test.go @@ -259,13 +259,38 @@ spec: }, }, }}, + }, { + name: "setting the controllerName for a GatewayClass", + applier: Applier{}, + given: ` +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: GatewayClass +metadata: + name: test +spec: + controllerName: {GATEWAY_CONTROLLER_NAME} +`, + expected: []unstructured.Unstructured{{ + Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1beta1", + "kind": "GatewayClass", + "metadata": map[string]interface{}{ + "name": "test", + }, + "spec": map[string]interface{}{ + "controllerName": "test-controller", + }, + }, + }}, }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(tc.given), 4096) - resources, err := tc.applier.prepareResources(t, decoder, "test-class") + tc.applier.GatewayClass = "test-class" + tc.applier.ControllerName = "test-controller" + resources, err := tc.applier.prepareResources(t, decoder) require.NoError(t, err, "unexpected error preparing resources") require.EqualValues(t, tc.expected, resources) diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go index 8f567d9fb4..df0a1e571f 100644 --- a/conformance/utils/kubernetes/helpers.go +++ b/conformance/utils/kubernetes/helpers.go @@ -18,6 +18,7 @@ package kubernetes import ( "context" + "errors" "fmt" "net" "reflect" @@ -81,6 +82,12 @@ func GWCMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.Timeo } controllerName = string(gwc.Spec.ControllerName) + + if err := ConditionsHaveLatestObservedGeneration(gwc, gwc.Status.Conditions); err != nil { + t.Log("GatewayClass", err) + return false, nil + } + // Passing an empty string as the Reason means that any Reason will do. return findConditionInList(t, gwc.Status.Conditions, "Accepted", "True", ""), nil }) @@ -89,6 +96,72 @@ func GWCMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.Timeo return controllerName } +// GatewayMustHaveLatestConditions will fail the test if there are +// conditions that were not updated +func GatewayMustHaveLatestConditions(t *testing.T, gw *v1beta1.Gateway) { + t.Helper() + + if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { + t.Fatalf("Gateway %v", err) + } +} + +// GatewayClassMustHaveLatestConditions will fail the test if there are +// conditions that were not updated +func GatewayClassMustHaveLatestConditions(t *testing.T, gwc *v1beta1.GatewayClass) { + t.Helper() + + if err := ConditionsHaveLatestObservedGeneration(gwc, gwc.Status.Conditions); err != nil { + t.Fatalf("GatewayClass %v", err) + } +} + +// HTTPRouteMustHaveLatestConditions will fail the test if there are +// conditions that were not updated +func HTTPRouteMustHaveLatestConditions(t *testing.T, r *v1beta1.HTTPRoute) { + t.Helper() + + for _, parent := range r.Status.Parents { + if err := ConditionsHaveLatestObservedGeneration(r, parent.Conditions); err != nil { + t.Fatalf("HTTPRoute(controller=%v, parentRef=%#v) %v", parent.ControllerName, parent, err) + } + } +} + +func ConditionsHaveLatestObservedGeneration(obj metav1.Object, conditions []metav1.Condition) error { + staleConditions := FilterStaleConditions(obj, conditions) + + if len(staleConditions) == 0 { + return nil + } + + var b strings.Builder + fmt.Fprint(&b, "expected observedGeneration to be updated for all conditions") + fmt.Fprintf(&b, ", only %d/%d were updated.", len(conditions)-len(staleConditions), len(conditions)) + fmt.Fprintf(&b, " stale conditions are: ") + + for i, c := range staleConditions { + fmt.Fprintf(&b, c.Type) + if i != len(staleConditions)-1 { + fmt.Fprintf(&b, ", ") + } + } + + return errors.New(b.String()) +} + +// FilterStaleConditions returns the list of status condition whos observedGeneration does not +// match the objects metadata.Generation +func FilterStaleConditions(obj metav1.Object, conditions []metav1.Condition) []metav1.Condition { + stale := make([]metav1.Condition, 0, len(conditions)) + for _, condition := range conditions { + if obj.GetGeneration() != condition.ObservedGeneration { + stale = append(stale, condition) + } + } + return stale +} + // NamespacesMustBeAccepted waits until all Pods are marked ready and all Gateways // are marked accepted in the provided namespaces. This will cause the test to // halt if the specified timeout is exceeded. @@ -106,6 +179,13 @@ func NamespacesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig confi t.Errorf("Error listing Gateways: %v", err) } for _, gw := range gwList.Items { + gw := gw + + if err = ConditionsHaveLatestObservedGeneration(&gw, gw.Status.Conditions); err != nil { + t.Log(err) + return false, nil + } + // Passing an empty string as the Reason means that any Reason will do. if !findConditionInList(t, gw.Status.Conditions, string(v1beta1.GatewayConditionAccepted), "True", "") { t.Logf("%s/%s Gateway not ready yet", ns, gw.Name) @@ -194,6 +274,11 @@ func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig con return false, fmt.Errorf("error fetching Gateway: %w", err) } + if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { + t.Log("Gateway", err) + return false, nil + } + port = strconv.FormatInt(int64(gw.Spec.Listeners[0].Port), 10) // TODO: Support more than IPAddress @@ -220,6 +305,12 @@ func GatewayMustHaveZeroRoutes(t *testing.T, client client.Client, timeoutConfig defer cancel() err := client.Get(ctx, gwName, gw) require.NoError(t, err, "error fetching Gateway") + + if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { + t.Log("Gateway ", err) + return false, nil + } + // There are two valid ways to represent this: // 1. No listeners in status // 2. One listener in status with 0 attached routes @@ -271,6 +362,14 @@ func HTTPRouteMustHaveNoAcceptedParents(t *testing.T, client client.Client, time // Only expect one parent return false, nil } + + for _, parent := range actual { + if err := ConditionsHaveLatestObservedGeneration(route, parent.Conditions); err != nil { + t.Logf("HTTPRoute(controller=%v,ref=%#v) %v", parent.ControllerName, parent, err) + return false, nil + } + } + return conditionsMatch(t, []metav1.Condition{{ Type: string(v1beta1.RouteConditionAccepted), Status: "False", @@ -296,8 +395,14 @@ func HTTPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig return false, fmt.Errorf("error fetching HTTPRoute: %w", err) } - actual = route.Status.Parents + for _, parent := range actual { + if err := ConditionsHaveLatestObservedGeneration(route, parent.Conditions); err != nil { + t.Logf("HTTPRoute(controller=%v,ref=%#v) %v", parent.ControllerName, parent, err) + return false, nil + } + } + actual = route.Status.Parents return parentsForRouteMatch(t, routeName, parents, actual, namespaceRequired), nil }) require.NoErrorf(t, waitErr, "error waiting for HTTPRoute to have parents matching expectations") @@ -363,8 +468,12 @@ func GatewayStatusMustHaveListeners(t *testing.T, client client.Client, timeoutC return false, fmt.Errorf("error fetching Gateway: %w", err) } - actual = gw.Status.Listeners + if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil { + t.Log("Gateway", err) + return false, nil + } + actual = gw.Status.Listeners return listenersMatch(t, listeners, actual), nil }) require.NoErrorf(t, waitErr, "error waiting for Gateway status to have listeners matching expectations") @@ -386,9 +495,14 @@ func HTTPRouteMustHaveCondition(t *testing.T, client client.Client, timeoutConfi } parents := route.Status.Parents - var conditionFound bool for _, parent := range parents { + if err := ConditionsHaveLatestObservedGeneration(route, parent.Conditions); err != nil { + + t.Logf("HTTPRoute(parentRef=%v) %v", parentRefToString(parent.ParentRef), err) + return false, nil + } + if parent.ParentRef.Name == v1beta1.ObjectName(gwNN.Name) && (parent.ParentRef.Namespace == nil || string(*parent.ParentRef.Namespace) == gwNN.Namespace) { if findConditionInList(t, parent.Conditions, condition.Type, string(condition.Status), condition.Reason) { conditionFound = true @@ -402,6 +516,13 @@ func HTTPRouteMustHaveCondition(t *testing.T, client client.Client, timeoutConfi require.NoErrorf(t, waitErr, "error waiting for HTTPRoute status to have a Condition matching expectations") } +func parentRefToString(p v1beta1.ParentReference) string { + if p.Namespace != nil && *p.Namespace != "" { + return fmt.Sprintf("%v/%v", p.Namespace, p.Name) + } + return string(p.Name) +} + // TODO(mikemorris): this and parentsMatch could possibly be rewritten as a generic function? func listenersMatch(t *testing.T, expected, actual []v1beta1.ListenerStatus) bool { t.Helper() diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index 42afe128d2..72f317a129 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -51,6 +51,9 @@ const ( // This option indicates support for Destination Port matching (extended conformance). SupportRouteDestinationPortMatching SupportedFeature = "RouteDestinationPortMatching" + + // This option indicates GatewayClass will update the observedGeneration in it's conditions when reconciling + SupportGatewayClassObservedGenerationBump SupportedFeature = "GatewayClassObservedGenerationBump" ) // StandardCoreFeatures are the features that are required to be conformant with @@ -145,8 +148,11 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T) { t.Logf("Test Setup: Ensuring GatewayClass has been accepted") suite.ControllerName = kubernetes.GWCMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.GatewayClassName) + suite.Applier.GatewayClass = suite.GatewayClassName + suite.Applier.ControllerName = suite.ControllerName + t.Logf("Test Setup: Applying base manifests") - suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.BaseManifests, suite.GatewayClassName, suite.Cleanup) + suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, suite.BaseManifests, suite.Cleanup) t.Logf("Test Setup: Applying programmatic resources") secret := kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-web-backend", "certificate", []string{"*"}) @@ -200,7 +206,7 @@ func (test *ConformanceTest) Run(t *testing.T, suite *ConformanceTestSuite) { for _, manifestLocation := range test.Manifests { t.Logf("Applying %s", manifestLocation) - suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, suite.GatewayClassName, true) + suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, true) } test.Test(t, suite)