Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions conformance/tests/backendtlspolicy-preserve-foreign-status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright 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 (
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
)

func init() {
ConformanceTests = append(ConformanceTests, BackendTLSPolicyPreserveForeignStatus)
}

var BackendTLSPolicyPreserveForeignStatus = confsuite.ConformanceTest{
ShortName: "BackendTLSPolicyPreserveForeignStatus",
Description: "An implementation must not remove or modify BackendTLSPolicy status.ancestors " +
"entries whose controllerName belongs to another implementation.",
Features: []features.FeatureName{
features.SupportGateway,
features.SupportHTTPRoute,
features.SupportBackendTLSPolicy,
},
Manifests: []string{"tests/backendtlspolicy-preserve-foreign-status.yaml"},
Provisional: true,
Test: func(t *testing.T, suite *confsuite.ConformanceTestSuite) {
ns := confsuite.InfrastructureNamespace
routeNN := types.NamespacedName{Name: "backendtlspolicy-preserve-foreign-status", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
policyNN := types.NamespacedName{Name: "backendtlspolicy-preserve-foreign-status", Namespace: ns}

kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
kubernetes.BackendTLSPolicyMustHaveAcceptedConditionTrue(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN)

var seeded gatewayv1.PolicyAncestorStatus
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
policy := &gatewayv1.BackendTLSPolicy{}
if err := suite.Client.Get(t.Context(), policyNN, policy); err != nil {
return err
}
policy.Status.Ancestors = append(policy.Status.Ancestors, gatewayv1.PolicyAncestorStatus{
AncestorRef: gatewayv1.ParentReference{
Group: new(gatewayv1.Group(gatewayv1.GroupVersion.Group)),
Kind: new(gatewayv1.Kind("Gateway")),
Name: "unmanaged-gateway",
Namespace: new(gatewayv1.Namespace(ns)),
},
ControllerName: kubernetes.StaleControllerName,
Conditions: []metav1.Condition{{
Type: string(gatewayv1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
Reason: string(gatewayv1.PolicyReasonAccepted),
ObservedGeneration: policy.Generation,
LastTransitionTime: metav1.Now(),
}},
})
if err := suite.Client.Status().Update(t.Context(), policy); err != nil {
return err
}
// Read the entry back off the update response rather than reusing
// the value written above: the API server truncates
// lastTransitionTime to second precision.
stored := foreignAncestorStatus(policy.Status.Ancestors)
if stored == nil {
return errForeignStatusNotStored
}
seeded = *stored
return nil
})
require.NoError(t, err, "error seeding a foreign controller's status entry on BackendTLSPolicy %s", policyNN)

// Bump the generation so the implementation has to run a full
// read-modify-write cycle on a status that already holds the seeded entry.
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
policy := &gatewayv1.BackendTLSPolicy{}
if getErr := suite.Client.Get(t.Context(), policyNN, policy); getErr != nil {
return getErr
}
policy.Spec.Validation.Hostname = "second.example.com"
return suite.Client.Update(t.Context(), policy)
})
require.NoError(t, err, "error updating BackendTLSPolicy %s", policyNN)

// The implementation's own entry catching up to the bumped generation is
// what proves it wrote status while the seeded entry was there.
kubernetes.BackendTLSPolicyMustHaveAcceptedConditionTrue(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN)

policy := &gatewayv1.BackendTLSPolicy{}
require.NoError(t, suite.Client.Get(t.Context(), policyNN, policy), "error fetching BackendTLSPolicy %s", policyNN)
kubernetes.BackendTLSPolicyMustHaveLatestConditions(t, policy)

stored := foreignAncestorStatus(policy.Status.Ancestors)
require.NotNilf(t, stored, "BackendTLSPolicy %s: status entry owned by %s was removed", policyNN, kubernetes.StaleControllerName)
require.Equalf(t, seeded, *stored, "BackendTLSPolicy %s: status entry owned by %s was modified", policyNN, kubernetes.StaleControllerName)
},
}

func foreignAncestorStatus(ancestors []gatewayv1.PolicyAncestorStatus) *gatewayv1.PolicyAncestorStatus {
for i := range ancestors {
if ancestors[i].ControllerName == kubernetes.StaleControllerName {
return &ancestors[i]
}
}

return nil
}
55 changes: 55 additions & 0 deletions conformance/tests/backendtlspolicy-preserve-foreign-status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: backendtlspolicy-preserve-foreign-status
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
namespace: gateway-conformance-infra
hostnames:
- preserve.example.com
rules:
- backendRefs:
- group: ""
kind: Service
name: backendtlspolicy-preserve-foreign-status-test
port: 443
matches:
- path:
type: Exact
value: /backendtlspolicy-preserve-foreign-status
---
apiVersion: v1
kind: Service
metadata:
name: backendtlspolicy-preserve-foreign-status-test
namespace: gateway-conformance-infra
spec:
selector:
app: tls-backend
ports:
- name: "btls"
protocol: TCP
port: 443
targetPort: 8443
---
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
name: backendtlspolicy-preserve-foreign-status
namespace: gateway-conformance-infra
spec:
targetRefs:
- group: ""
kind: Service
name: "backendtlspolicy-preserve-foreign-status-test"
sectionName: "btls"
validation:
caCertificateRefs:
- group: ""
kind: ConfigMap
# This ConfigMap is generated dynamically by the test suite.
# It contains the CA certificate used to sign the tls-backend serving certificate.
name: "tls-checks-ca-certificate"
hostname: "abc.example.com"
179 changes: 179 additions & 0 deletions conformance/tests/gateway-preserve-foreign-conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*
Copyright 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"
"errors"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
confsuite "sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
)

var errForeignConditionNotStored = errors.New("the seeded condition is missing from the update response")

func init() {
ConformanceTests = append(ConformanceTests, GatewayPreserveForeignConditions)
}

var GatewayPreserveForeignConditions = confsuite.ConformanceTest{
ShortName: "GatewayPreserveForeignConditions",
Description: "An implementation must not remove or modify Gateway and Listener status conditions " +
"whose type it is not responsible for.",
Features: []features.FeatureName{
features.SupportGateway,
},
Manifests: []string{"tests/gateway-preserve-foreign-conditions.yaml"},
Provisional: true,
Test: func(t *testing.T, suite *confsuite.ConformanceTestSuite) {
ns := confsuite.InfrastructureNamespace
gwNN := types.NamespacedName{Name: "gateway-preserve-foreign-conditions", Namespace: ns}
listenerName := gatewayv1.SectionName("http")

kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
kubernetes.GatewayMustHaveLatestConditions(t, suite.Client, suite.TimeoutConfig, gwNN)

// Listener status is where the second seeded condition goes, so its
// entry has to exist first.
waitErr := wait.PollUntilContextTimeout(context.Background(), suite.TimeoutConfig.DefaultPollInterval, suite.TimeoutConfig.GatewayStatusMustHaveListeners, true, func(ctx context.Context) (bool, error) {
gw := &gatewayv1.Gateway{}
if err := suite.Client.Get(ctx, gwNN, gw); err != nil {
return false, err
}
return listenerStatus(gw, listenerName) != nil, nil
})
require.NoErrorf(t, waitErr, "error waiting for Gateway %s to report status for listener %s", gwNN, listenerName)

foreignCondition := func(observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: kubernetes.StaleConditionType,
Status: metav1.ConditionTrue,
Reason: "Seeded",
ObservedGeneration: observedGeneration,
LastTransitionTime: metav1.Now(),
}
}

var seededGw, seededListener metav1.Condition
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
gw := &gatewayv1.Gateway{}
if err := suite.Client.Get(t.Context(), gwNN, gw); err != nil {
return err
}
ls := listenerStatus(gw, listenerName)
if ls == nil {
return errForeignConditionNotStored
}
gw.Status.Conditions = append(gw.Status.Conditions, foreignCondition(gw.Generation))
ls.Conditions = append(ls.Conditions, foreignCondition(gw.Generation))
if err := suite.Client.Status().Update(t.Context(), gw); err != nil {
return err
}
// Read the conditions back off the update response rather than
// reusing the values written above: the API server truncates
// lastTransitionTime to second precision.
storedGw := foreignConditionIn(gw.Status.Conditions)
ls = listenerStatus(gw, listenerName)
if storedGw == nil || ls == nil {
return errForeignConditionNotStored
}
storedListener := foreignConditionIn(ls.Conditions)
if storedListener == nil {
return errForeignConditionNotStored
}
seededGw = *storedGw
seededListener = *storedListener
return nil
})
require.NoError(t, err, "error seeding foreign conditions on Gateway %s", gwNN)

// Bump the generation so the implementation has to run a full
// read-modify-write cycle on a status that already holds the seeded
// conditions.
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
gw := &gatewayv1.Gateway{}
if getErr := suite.Client.Get(t.Context(), gwNN, gw); getErr != nil {
return getErr
}
gw.Spec.Listeners[0].Hostname = new(gatewayv1.Hostname("second.example.com"))
return suite.Client.Update(t.Context(), gw)
})
require.NoError(t, err, "error updating Gateway %s", gwNN)

// The implementation's own conditions catching up to the bumped
// generation, on the Gateway and on every listener, is what proves it
// wrote status while the seeded conditions were there.
waitErr = wait.PollUntilContextTimeout(context.Background(), suite.TimeoutConfig.DefaultPollInterval, suite.TimeoutConfig.LatestObservedGenerationSet, true, func(ctx context.Context) (bool, error) {
gw := &gatewayv1.Gateway{}
if err := suite.Client.Get(ctx, gwNN, gw); err != nil {
return false, err
}
if err := kubernetes.ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil {
return false, nil
}
for _, ls := range gw.Status.Listeners {
if err := kubernetes.ConditionsHaveLatestObservedGeneration(gw, ls.Conditions); err != nil {
return false, nil
}
}
return true, nil
})
require.NoErrorf(t, waitErr, "error waiting for Gateway %s conditions to catch up with the bumped generation", gwNN)

gw := &gatewayv1.Gateway{}
require.NoError(t, suite.Client.Get(t.Context(), gwNN, gw), "error fetching Gateway %s", gwNN)

stored := foreignConditionIn(gw.Status.Conditions)
require.NotNilf(t, stored, "Gateway %s: the %s condition was removed", gwNN, kubernetes.StaleConditionType)
require.Equalf(t, seededGw, *stored, "Gateway %s: the %s condition was modified", gwNN, kubernetes.StaleConditionType)

ls := listenerStatus(gw, listenerName)
require.NotNilf(t, ls, "Gateway %s: status for listener %s is missing", gwNN, listenerName)
stored = foreignConditionIn(ls.Conditions)
require.NotNilf(t, stored, "Gateway %s listener %s: the %s condition was removed", gwNN, listenerName, kubernetes.StaleConditionType)
require.Equalf(t, seededListener, *stored, "Gateway %s listener %s: the %s condition was modified", gwNN, listenerName, kubernetes.StaleConditionType)
},
}

func listenerStatus(gw *gatewayv1.Gateway, name gatewayv1.SectionName) *gatewayv1.ListenerStatus {
for i := range gw.Status.Listeners {
if gw.Status.Listeners[i].Name == name {
return &gw.Status.Listeners[i]
}
}

return nil
}

func foreignConditionIn(conditions []metav1.Condition) *metav1.Condition {
for i := range conditions {
if conditions[i].Type == kubernetes.StaleConditionType {
return &conditions[i]
}
}

return nil
}
14 changes: 14 additions & 0 deletions conformance/tests/gateway-preserve-foreign-conditions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway-preserve-foreign-conditions
namespace: gateway-conformance-infra
spec:
gatewayClassName: "{GATEWAY_CLASS_NAME}"
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same
Loading