88 "fmt"
99 "strings"
1010 "testing"
11+ "time"
1112
1213 "github.com/openshift/api/features"
1314 operatorclient "github.com/openshift/cluster-ingress-operator/pkg/operator/client"
@@ -18,8 +19,10 @@ import (
1819 "k8s.io/apimachinery/pkg/api/errors"
1920 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021 "k8s.io/apimachinery/pkg/types"
22+ "k8s.io/apimachinery/pkg/util/wait"
2123 "k8s.io/apiserver/pkg/storage/names"
2224 "k8s.io/client-go/rest"
25+ "k8s.io/utils/ptr"
2326
2427 "sigs.k8s.io/controller-runtime/pkg/client"
2528 "sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -96,9 +99,10 @@ func TestGatewayAPI(t *testing.T) {
9699 t .Run ("testGatewayAPIResources" , testGatewayAPIResources )
97100 if gatewayAPIControllerEnabled {
98101 t .Run ("testGatewayAPIObjects" , testGatewayAPIObjects )
102+ t .Run ("testGatewayAPIManualDeployment" , testGatewayAPIManualDeployment )
99103 t .Run ("testGatewayAPIIstioInstallation" , testGatewayAPIIstioInstallation )
100104 } else {
101- t .Log ("Gateway API Controller not enabled, skipping testGatewayAPIObjects and testGatewayAPIIstioInstallation " )
105+ t .Log ("Gateway API Controller not enabled, skipping controller tests " )
102106 }
103107 t .Run ("testGatewayAPIResourcesProtection" , testGatewayAPIResourcesProtection )
104108}
@@ -191,6 +195,128 @@ func testGatewayAPIObjects(t *testing.T) {
191195 }
192196}
193197
198+ // testGatewayAPIManualDeployment verifies that Istio's "manual deployment"
199+ // feature is not enabled (see
200+ // <https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#manual-deployment>).
201+ // We only want Istio to allow "automated deployment" (see
202+ // <https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#automated-deployment>).
203+ //
204+ // When manual deployment is enabled, then Istio allows a gateway to use an
205+ // existing service (for example, another gateway's service) by specifying that
206+ // service in spec.addresses. When a gateway using manual deployment specifies
207+ // another gateway's service, the resulting behavior is effectively the same
208+ // behavior as Gateway API's concept of gateway listener "merging" (see
209+ // <https://github.com/kubernetes-sigs/gateway-api/blob/v1.2.1/apis/v1/gateway_types.go#L181-L182>).
210+ //
211+ // Gateway listener merging is underspecified in Gateway API and is not
212+ // consistently implemented among Gateway API implementations, and so we do not
213+ // want to allow it or any similar behavior (such as Istio's "manual
214+ // deployment") until such a time as it is well defined, standard behavior.
215+ // Instead, for the time being, we expect Istio to provision a service for a
216+ // gateway ("automated deployment"), even if the gateway specifies some existing
217+ // service in spec.addresses.
218+ func testGatewayAPIManualDeployment (t * testing.T ) {
219+ gatewayClass , err := createGatewayClass ("openshift-default" , "openshift.io/gateway-controller/v1" )
220+ if err != nil {
221+ t .Fatalf ("Failed to create gatewayclass: %v" , err )
222+ }
223+
224+ gatewayName := types.NamespacedName {
225+ Name : "manual-deployment" ,
226+ Namespace : "openshift-ingress" ,
227+ }
228+ // Use the router's internal service in order to ensure that the
229+ // referent exists. Using an existing service isn't strictly necessary
230+ // in order to verify that Istio does not use manual deployment; if
231+ // manual deployment *is* enabled, Istio rejects the gateway if it
232+ // points to a non-existent referent. However, using an existing
233+ // service more closely reflects the way that manual deployment *would*
234+ // be used if it were allowed.
235+ const existingServiceHostname = "router-internal-default.openshift-ingress.svc.cluster.local"
236+ gateway := gatewayapiv1.Gateway {
237+ ObjectMeta : metav1.ObjectMeta {
238+ Name : gatewayName .Name ,
239+ Namespace : gatewayName .Namespace ,
240+ },
241+ Spec : gatewayapiv1.GatewaySpec {
242+ GatewayClassName : gatewayapiv1 .ObjectName (gatewayClass .Name ),
243+ Addresses : []gatewayapiv1.GatewayAddress {{
244+ Type : ptr .To (gatewayapiv1 .HostnameAddressType ),
245+ Value : existingServiceHostname ,
246+ }},
247+ Listeners : []gatewayapiv1.Listener {{
248+ Name : "http" ,
249+ Hostname : ptr .To (gatewayapiv1 .Hostname (fmt .Sprintf ("*.manual-deployment.%s" , dnsConfig .Spec .BaseDomain ))),
250+ Port : 80 ,
251+ Protocol : "HTTP" ,
252+ }},
253+ },
254+ }
255+ t .Logf ("Creating gateway %q..." , gatewayName )
256+ if err := kclient .Create (context .Background (), & gateway ); err != nil {
257+ t .Fatalf ("Failed to create gateway %v: %v" , gatewayName , err )
258+ }
259+ t .Cleanup (func () {
260+ if err := kclient .Delete (context .Background (), & gateway ); err != nil {
261+ if ! errors .IsNotFound (err ) {
262+ t .Errorf ("Failed to delete gateway %v: %v" , gatewayName , err )
263+ }
264+ }
265+ })
266+
267+ interval , timeout := 5 * time .Second , 1 * time .Minute
268+ t .Logf ("Polling for up to %v to verify that the gateway is accepted..." , timeout )
269+ if err := wait .PollUntilContextTimeout (context .Background (), interval , timeout , false , func (context context.Context ) (bool , error ) {
270+ if err := kclient .Get (context , gatewayName , & gateway ); err != nil {
271+ t .Logf ("Failed to get gateway %v: %v; retrying..." , gatewayName , err )
272+
273+ return false , nil
274+ }
275+
276+ for _ , condition := range gateway .Status .Conditions {
277+ if condition .Type == string (gatewayapiv1 .GatewayConditionAccepted ) {
278+ t .Logf ("Found %q status condition: %+v" , gatewayapiv1 .GatewayConditionAccepted , condition )
279+
280+ if condition .Status == metav1 .ConditionTrue {
281+ return true , nil
282+ }
283+ }
284+ }
285+
286+ t .Logf ("Observed that gateway %v is not yet accepted; retrying..." , gatewayName )
287+
288+ return false , nil
289+ }); err != nil {
290+ t .Errorf ("Failed to observe the expected condition for gateway %v: %v" , gatewayName , err )
291+ }
292+
293+ serviceName := types.NamespacedName {
294+ Name : fmt .Sprintf ("%s-%s" , gateway .Name , gatewayClass .Name ),
295+ Namespace : gateway .Namespace ,
296+ }
297+ var service corev1.Service
298+ t .Logf ("Polling for up to %v to verify that service %q is created..." , timeout , serviceName )
299+ if err := wait .PollUntilContextTimeout (context .Background (), interval , timeout , false , func (context context.Context ) (bool , error ) {
300+ if err := kclient .Get (context , serviceName , & service ); err != nil {
301+ t .Logf ("Failed to get service %s: %v; retrying..." , serviceName , err )
302+
303+ return false , nil
304+ }
305+
306+ // Just verify that the service is created. No need to verify
307+ // that a load balancer is provisioned. Indeed, provisioning
308+ // will likely fail because Istio copies the address hostname to
309+ // the service spec.loadBalancerIP field, which at least some
310+ // cloud provider implementations reject.
311+
312+ t .Logf ("Found service %q" , serviceName )
313+
314+ return true , nil
315+ }); err != nil {
316+ t .Errorf ("Failed to observe the expected condition for service %v: %v" , serviceName , err )
317+ }
318+ }
319+
194320// testGatewayAPIResourcesProtection verifies that the ingress operator's Validating Admission Policy
195321// denies admission requests attempting to modify Gateway API CRDs on behalf of a user
196322// who is not the ingress operator's service account.
0 commit comments