Skip to content

Commit dc314b1

Browse files
authored
Assert observedGeneration is incremented in Status.Conditions. (#1586)
* Update helpers so they check conditions when they are up to date Prior when asserting against conditions the helpers didn't check their observedGeneration. This means the assertions could occur with a stale status. * When updating Gateway status conditions should increment their observedGeneration * allow injection of controllerNames * When updating GatewayClass the status conditions should increment their observedGeneration * fix casing of t.Error/Fatal messages * removed pasted link * refactor some helpers to assert observedGeneration is bumped * add HTTPRoute observed generation tests * use a unique port when adding a new listener * increase test timeout to a minute * use real backends * fail test is an unexpected parent ref appears * Provide better error messages for failed assertions * fix fixture name * fix stale count logging * create a deep copy prior to mutation and updating * drop redundant assertions *MustHaveLatestConditions will ensure the observedGeneration has changed * address linting * use assertion helper * log only the parent ref name * update godoc * put the gateway class observed gen bump behind a support flag
1 parent 9c9429a commit dc314b1

10 files changed

+463
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2022 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 tests
18+
19+
import (
20+
"context"
21+
"testing"
22+
"time"
23+
24+
"github.com/stretchr/testify/require"
25+
"k8s.io/apimachinery/pkg/types"
26+
27+
"sigs.k8s.io/gateway-api/apis/v1beta1"
28+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
29+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
30+
)
31+
32+
func init() {
33+
ConformanceTests = append(ConformanceTests, GatewayObservedGenerationBump)
34+
}
35+
36+
var GatewayObservedGenerationBump = suite.ConformanceTest{
37+
ShortName: "GatewayObservedGenerationBump",
38+
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",
39+
Manifests: []string{"tests/gateway-observed-generation-bump.yaml"},
40+
Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
41+
42+
gwNN := types.NamespacedName{Name: "gateway-observed-generation-bump", Namespace: "gateway-conformance-infra"}
43+
44+
t.Run("observedGeneration should increment", func(t *testing.T) {
45+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
46+
defer cancel()
47+
48+
namespaces := []string{"gateway-conformance-infra"}
49+
kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces)
50+
51+
original := &v1beta1.Gateway{}
52+
err := s.Client.Get(ctx, gwNN, original)
53+
require.NoErrorf(t, err, "error getting Gateway: %v", err)
54+
55+
// Sanity check
56+
kubernetes.GatewayMustHaveLatestConditions(t, original)
57+
58+
all := v1beta1.NamespacesFromAll
59+
60+
mutate := original.DeepCopy()
61+
62+
// mutate the Gateway Spec
63+
mutate.Spec.Listeners = append(mutate.Spec.Listeners, v1beta1.Listener{
64+
Name: "alternate",
65+
Port: 8080,
66+
Protocol: v1beta1.HTTPProtocolType,
67+
AllowedRoutes: &v1beta1.AllowedRoutes{
68+
Namespaces: &v1beta1.RouteNamespaces{From: &all},
69+
},
70+
})
71+
72+
err = s.Client.Update(ctx, mutate)
73+
require.NoErrorf(t, err, "error updating the Gateway: %v", err)
74+
75+
// Ensure the generation and observedGeneration sync up
76+
kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces)
77+
78+
updated := &v1beta1.Gateway{}
79+
err = s.Client.Get(ctx, gwNN, updated)
80+
require.NoErrorf(t, err, "error getting Gateway: %v", err)
81+
82+
// Sanity check
83+
kubernetes.GatewayMustHaveLatestConditions(t, updated)
84+
85+
require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update")
86+
})
87+
},
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: Gateway
3+
metadata:
4+
name: gateway-observed-generation-bump
5+
namespace: gateway-conformance-infra
6+
spec:
7+
gatewayClassName: "{GATEWAY_CLASS_NAME}"
8+
listeners:
9+
- name: http
10+
port: 80
11+
protocol: HTTP
12+
allowedRoutes:
13+
namespaces:
14+
from: All
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2022 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 tests
18+
19+
import (
20+
"context"
21+
"testing"
22+
"time"
23+
24+
"github.com/stretchr/testify/require"
25+
"k8s.io/apimachinery/pkg/types"
26+
27+
"sigs.k8s.io/gateway-api/apis/v1beta1"
28+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
29+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
30+
)
31+
32+
func init() {
33+
ConformanceTests = append(ConformanceTests, GatewayClassObservedGenerationBump)
34+
}
35+
36+
var GatewayClassObservedGenerationBump = suite.ConformanceTest{
37+
ShortName: "GatewayClassObservedGenerationBump",
38+
Features: []suite.SupportedFeature{suite.SupportGatewayClassObservedGenerationBump},
39+
Description: "A GatewayClass should update the observedGeneration in all of it's Status.Conditions after an update to the spec",
40+
Manifests: []string{"tests/gatewayclass-observed-generation-bump.yaml"},
41+
Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
42+
gwc := types.NamespacedName{Name: "gatewayclass-observed-generation-bump"}
43+
44+
t.Run("observedGeneration should increment", func(t *testing.T) {
45+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
46+
defer cancel()
47+
48+
kubernetes.GWCMustBeAccepted(t, s.Client, s.TimeoutConfig, gwc.Name)
49+
50+
original := &v1beta1.GatewayClass{}
51+
err := s.Client.Get(ctx, gwc, original)
52+
require.NoErrorf(t, err, "error getting GatewayClass: %v", err)
53+
54+
// Sanity check
55+
kubernetes.GatewayClassMustHaveLatestConditions(t, original)
56+
57+
mutate := original.DeepCopy()
58+
desc := "new"
59+
mutate.Spec.Description = &desc
60+
61+
err = s.Client.Update(ctx, mutate)
62+
require.NoErrorf(t, err, "error updating the GatewayClass: %v", err)
63+
64+
// Ensure the generation and observedGeneration sync up
65+
kubernetes.GWCMustBeAccepted(t, s.Client, s.TimeoutConfig, gwc.Name)
66+
67+
updated := &v1beta1.GatewayClass{}
68+
err = s.Client.Get(ctx, gwc, updated)
69+
require.NoErrorf(t, err, "error getting GatewayClass: %v", err)
70+
71+
// Sanity check
72+
kubernetes.GatewayClassMustHaveLatestConditions(t, updated)
73+
74+
require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update")
75+
})
76+
},
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: GatewayClass
3+
metadata:
4+
name: gatewayclass-observed-generation-bump
5+
spec:
6+
controllerName: "{GATEWAY_CONTROLLER_NAME}"
7+
description: "old"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright 2022 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 tests
18+
19+
import (
20+
"context"
21+
"testing"
22+
"time"
23+
24+
"github.com/stretchr/testify/require"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/types"
27+
28+
"sigs.k8s.io/gateway-api/apis/v1beta1"
29+
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
30+
"sigs.k8s.io/gateway-api/conformance/utils/suite"
31+
)
32+
33+
func init() {
34+
ConformanceTests = append(ConformanceTests, HTTPRouteObservedGenerationBump)
35+
}
36+
37+
var HTTPRouteObservedGenerationBump = suite.ConformanceTest{
38+
ShortName: "HTTPRouteObservedGenerationBump",
39+
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",
40+
Manifests: []string{"tests/httproute-observed-generation-bump.yaml"},
41+
Test: func(t *testing.T, s *suite.ConformanceTestSuite) {
42+
43+
routeNN := types.NamespacedName{Name: "observed-generation-bump", Namespace: "gateway-conformance-infra"}
44+
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: "gateway-conformance-infra"}
45+
46+
acceptedCondition := metav1.Condition{
47+
Type: string(v1beta1.RouteConditionAccepted),
48+
Status: metav1.ConditionTrue,
49+
Reason: "", // any reason
50+
}
51+
52+
t.Run("observedGeneration should increment", func(t *testing.T) {
53+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
54+
defer cancel()
55+
56+
namespaces := []string{"gateway-conformance-infra"}
57+
kubernetes.NamespacesMustBeAccepted(t, s.Client, s.TimeoutConfig, namespaces)
58+
59+
original := &v1beta1.HTTPRoute{}
60+
err := s.Client.Get(ctx, routeNN, original)
61+
require.NoErrorf(t, err, "error getting HTTPRoute: %v", err)
62+
63+
// Sanity check
64+
kubernetes.HTTPRouteMustHaveLatestConditions(t, original)
65+
66+
mutate := original.DeepCopy()
67+
mutate.Spec.Rules[0].BackendRefs[0].Name = "infra-backend-v2"
68+
err = s.Client.Update(ctx, mutate)
69+
require.NoErrorf(t, err, "error updating the HTTPRoute: %v", err)
70+
71+
kubernetes.HTTPRouteMustHaveCondition(t, s.Client, s.TimeoutConfig, routeNN, gwNN, acceptedCondition)
72+
73+
updated := &v1beta1.HTTPRoute{}
74+
err = s.Client.Get(ctx, routeNN, updated)
75+
require.NoErrorf(t, err, "error getting Gateway: %v", err)
76+
77+
// Sanity check
78+
kubernetes.HTTPRouteMustHaveLatestConditions(t, updated)
79+
80+
require.NotEqual(t, original.Generation, updated.Generation, "generation should change after an update")
81+
})
82+
},
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: gateway.networking.k8s.io/v1beta1
2+
kind: HTTPRoute
3+
metadata:
4+
name: observed-generation-bump
5+
namespace: gateway-conformance-infra
6+
spec:
7+
parentRefs:
8+
- name: same-namespace
9+
rules:
10+
- backendRefs:
11+
- name: infra-backend-v1
12+
port: 8080

conformance/utils/kubernetes/apply.go

+24-9
Original file line numberDiff line numberDiff line change
@@ -49,25 +49,31 @@ type Applier struct {
4949
// four ValidUniqueListenerPorts.
5050
// If empty or nil, ports are not modified.
5151
ValidUniqueListenerPorts []v1beta1.PortNumber
52+
53+
// GatewayClass will be used as the spec.gatewayClassName when applying Gateway resources
54+
GatewayClass string
55+
56+
// ControllerName will be used as the spec.controllerName when applying GatewayClass resources
57+
ControllerName string
5258
}
5359

5460
// prepareGateway adjusts both listener ports and the gatewayClassName. It
5561
// returns an index pointing to the next valid listener port.
56-
func prepareGateway(t *testing.T, uObj *unstructured.Unstructured, gatewayClassName string, validListenerPorts []v1beta1.PortNumber, portIndex int) int {
57-
err := unstructured.SetNestedField(uObj.Object, gatewayClassName, "spec", "gatewayClassName")
62+
func (a Applier) prepareGateway(t *testing.T, uObj *unstructured.Unstructured, portIndex int) int {
63+
err := unstructured.SetNestedField(uObj.Object, a.GatewayClass, "spec", "gatewayClassName")
5864
require.NoErrorf(t, err, "error setting `spec.gatewayClassName` on %s Gateway resource", uObj.GetName())
5965

60-
if len(validListenerPorts) > 0 {
66+
if len(a.ValidUniqueListenerPorts) > 0 {
6167
listeners, _, err := unstructured.NestedSlice(uObj.Object, "spec", "listeners")
6268
require.NoErrorf(t, err, "error getting `spec.listeners` on %s Gateway resource", uObj.GetName())
6369

6470
for i, uListener := range listeners {
65-
require.Less(t, portIndex, len(validListenerPorts), "not enough unassigned valid ports for `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName())
71+
require.Less(t, portIndex, len(a.ValidUniqueListenerPorts), "not enough unassigned valid ports for `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName())
6672

6773
listener, ok := uListener.(map[string]interface{})
6874
require.Truef(t, ok, "unexpected type at `spec.listeners[%d]` on %s Gateway resource", i, uObj.GetName())
6975

70-
nextPort := validListenerPorts[portIndex]
76+
nextPort := a.ValidUniqueListenerPorts[portIndex]
7177
err = unstructured.SetNestedField(listener, int64(nextPort), "port")
7278
require.NoErrorf(t, err, "error setting `spec.listeners[%d].port` on %s Gateway resource", i, uObj.GetName())
7379

@@ -82,6 +88,12 @@ func prepareGateway(t *testing.T, uObj *unstructured.Unstructured, gatewayClassN
8288
return portIndex
8389
}
8490

91+
// prepareGatewayClass adjust the spec.controllerName on the resource
92+
func (a Applier) prepareGatewayClass(t *testing.T, uObj *unstructured.Unstructured) {
93+
err := unstructured.SetNestedField(uObj.Object, a.ControllerName, "spec", "controllerName")
94+
require.NoErrorf(t, err, "error setting `spec.controllerName` on %s GatewayClass resource", uObj.GetName())
95+
}
96+
8597
// prepareNamespace adjusts the Namespace labels.
8698
func prepareNamespace(t *testing.T, uObj *unstructured.Unstructured, namespaceLabels map[string]string) {
8799
labels, _, err := unstructured.NestedStringMap(uObj.Object, "metadata", "labels")
@@ -104,7 +116,7 @@ func prepareNamespace(t *testing.T, uObj *unstructured.Unstructured, namespaceLa
104116

105117
// prepareResources uses the options from an Applier to tweak resources given by
106118
// a set of manifests.
107-
func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder, gcName string) ([]unstructured.Unstructured, error) {
119+
func (a Applier) prepareResources(t *testing.T, decoder *yaml.YAMLOrJSONDecoder) ([]unstructured.Unstructured, error) {
108120
var resources []unstructured.Unstructured
109121

110122
// 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,
123135
continue
124136
}
125137

138+
if uObj.GetKind() == "GatewayClass" {
139+
a.prepareGatewayClass(t, &uObj)
140+
}
126141
if uObj.GetKind() == "Gateway" {
127-
portIndex = prepareGateway(t, &uObj, gcName, a.ValidUniqueListenerPorts, portIndex)
142+
portIndex = a.prepareGateway(t, &uObj, portIndex)
128143
}
129144

130145
if uObj.GetKind() == "Namespace" && uObj.GetObjectKind().GroupVersionKind().Group == "" {
@@ -168,13 +183,13 @@ func (a Applier) MustApplyObjectsWithCleanup(t *testing.T, c client.Client, time
168183
// MustApplyWithCleanup creates or updates Kubernetes resources defined with the
169184
// provided YAML file and registers a cleanup function for resources it created.
170185
// Note that this does not remove resources that already existed in the cluster.
171-
func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, gcName string, cleanup bool) {
186+
func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, cleanup bool) {
172187
data, err := getContentsFromPathOrURL(location, timeoutConfig)
173188
require.NoError(t, err)
174189

175190
decoder := yaml.NewYAMLOrJSONDecoder(data, 4096)
176191

177-
resources, err := a.prepareResources(t, decoder, gcName)
192+
resources, err := a.prepareResources(t, decoder)
178193
if err != nil {
179194
t.Logf("manifest: %s", data.String())
180195
require.NoErrorf(t, err, "error parsing manifest")

0 commit comments

Comments
 (0)