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,107 @@ func testGatewayAPIObjects(t *testing.T) {
191195 }
192196}
193197
198+ // testGatewayAPIManualDeployment verifies that Istio's manual deployment is not
199+ // enabled. When manual deployment is enabled, then Istio allows a gateway to
200+ // use another gateway's service by specifying that gateway's service in
201+ // spec.addresses. This results in behavior similar to gateway listener
202+ // merging, which we do not want to allow at this time. Instead, we expect
203+ // Istio to provision a service for this specific gateway, even if it specifies
204+ // spec.addresses.
205+ func testGatewayAPIManualDeployment (t * testing.T ) {
206+ gatewayClass , err := createGatewayClass ("openshift-default" , "openshift.io/gateway-controller/v1" )
207+ if err != nil {
208+ t .Fatalf ("Failed to create gatewayclass: %v" , err )
209+ }
210+
211+ gatewayName := types.NamespacedName {
212+ Name : "manual-deployment" ,
213+ Namespace : "openshift-ingress" ,
214+ }
215+ gateway := gatewayapiv1.Gateway {
216+ ObjectMeta : metav1.ObjectMeta {
217+ Name : gatewayName .Name ,
218+ Namespace : gatewayName .Namespace ,
219+ },
220+ Spec : gatewayapiv1.GatewaySpec {
221+ GatewayClassName : gatewayapiv1 .ObjectName (gatewayClass .Name ),
222+ Addresses : []gatewayapiv1.GatewayAddress {{
223+ Type : ptr .To (gatewayapiv1 .HostnameAddressType ),
224+ Value : "lb.example.com" ,
225+ }},
226+ Listeners : []gatewayapiv1.Listener {{
227+ Name : "http" ,
228+ Hostname : ptr .To (gatewayapiv1 .Hostname (fmt .Sprintf ("*.manual-deployment.%s" , dnsConfig .Spec .BaseDomain ))),
229+ Port : 80 ,
230+ Protocol : "HTTP" ,
231+ }},
232+ },
233+ }
234+ t .Logf ("Creating gateway %q..." , gatewayName )
235+ if err := kclient .Create (context .Background (), & gateway ); err != nil {
236+ t .Fatalf ("Failed to create gateway %v: %v" , gatewayName , err )
237+ }
238+ t .Cleanup (func () {
239+ if err := kclient .Delete (context .Background (), & gateway ); err != nil {
240+ if ! errors .IsNotFound (err ) {
241+ t .Errorf ("Failed to delete gateway %v: %v" , gatewayName , err )
242+ }
243+ }
244+ })
245+
246+ interval , timeout := 5 * time .Second , 1 * time .Minute
247+ t .Logf ("Polling for up to %v to verify that the gateway is accepted..." , timeout )
248+ if err := wait .PollUntilContextTimeout (context .Background (), interval , timeout , false , func (context context.Context ) (bool , error ) {
249+ if err := kclient .Get (context , gatewayName , & gateway ); err != nil {
250+ t .Logf ("Failed to get gateway %v: %v; retrying..." , gatewayName , err )
251+
252+ return false , nil
253+ }
254+
255+ for _ , condition := range gateway .Status .Conditions {
256+ if condition .Type == string (gatewayapiv1 .GatewayConditionAccepted ) {
257+ t .Logf ("Found %q status condition: %+v" , gatewayapiv1 .GatewayConditionAccepted , condition )
258+
259+ if condition .Status == metav1 .ConditionTrue {
260+ return true , nil
261+ }
262+ }
263+ }
264+
265+ t .Logf ("Observed that gateway %v is not yet accepted; retrying..." , gatewayName )
266+
267+ return false , nil
268+ }); err != nil {
269+ t .Errorf ("Failed to observe the expected condition for gateway %v: %v" , gatewayName , err )
270+ }
271+
272+ serviceName := types.NamespacedName {
273+ Name : fmt .Sprintf ("%s-%s" , gateway .Name , gatewayClass .Name ),
274+ Namespace : gateway .Namespace ,
275+ }
276+ var service corev1.Service
277+ t .Logf ("Polling for up to %v to verify that service %q is created..." , timeout , serviceName )
278+ if err := wait .PollUntilContextTimeout (context .Background (), interval , timeout , false , func (context context.Context ) (bool , error ) {
279+ if err := kclient .Get (context , serviceName , & service ); err != nil {
280+ t .Logf ("Failed to get service %s: %v; retrying..." , serviceName , err )
281+
282+ return false , nil
283+ }
284+
285+ // Just verify that the service is created. No need to verify
286+ // that a load balancer is provisioned. Indeed, provisioning
287+ // will likely fail because Istio copies the address hostname to
288+ // the service spec.loadBalancerIP field, which at least some
289+ // cloud provider implementations reject.
290+
291+ t .Logf ("Found service %q" , serviceName )
292+
293+ return true , nil
294+ }); err != nil {
295+ t .Errorf ("Failed to observe the expected condition for service %v: %v" , serviceName , err )
296+ }
297+ }
298+
194299// testGatewayAPIResourcesProtection verifies that the ingress operator's Validating Admission Policy
195300// denies admission requests attempting to modify Gateway API CRDs on behalf of a user
196301// who is not the ingress operator's service account.
0 commit comments