Browse code

o Split out cert list and use commit from PR 11217 o Allow wildcard (currently only *.) routes to be created and add tests. o Add a host admission controller and allow/deny list of domains and control the admission/blockage of wildcard routes. o Fix test cases and expection. o Add helper to generate valid wildcard regular expressions. o Add wildcard domain map + regex based rules and use the rules for wildcard routes. o Bug fixes and add tests. o Add generated completions and docs. o Changes as per @marun, @rajatchopra, @smarterclayton review comments

ramr authored on 2016/10/14 08:22:59
Showing 18 changed files
... ...
@@ -19225,6 +19225,10 @@ _openshift_infra_f5-router()
19225 19225
     flags_with_completion=()
19226 19226
     flags_completion=()
19227 19227
 
19228
+    flags+=("--allow-wildcard-routes")
19229
+    local_nonpersistent_flags+=("--allow-wildcard-routes")
19230
+    flags+=("--allowed-domains=")
19231
+    local_nonpersistent_flags+=("--allowed-domains=")
19228 19232
     flags+=("--as=")
19229 19233
     local_nonpersistent_flags+=("--as=")
19230 19234
     flags+=("--certificate-authority=")
... ...
@@ -19247,6 +19251,8 @@ _openshift_infra_f5-router()
19247 19247
     local_nonpersistent_flags+=("--config=")
19248 19248
     flags+=("--context=")
19249 19249
     local_nonpersistent_flags+=("--context=")
19250
+    flags+=("--denied-domains=")
19251
+    local_nonpersistent_flags+=("--denied-domains=")
19250 19252
     flags+=("--f5-host=")
19251 19253
     local_nonpersistent_flags+=("--f5-host=")
19252 19254
     flags+=("--f5-http-vserver=")
... ...
@@ -19360,6 +19366,10 @@ _openshift_infra_router()
19360 19360
     flags_with_completion=()
19361 19361
     flags_completion=()
19362 19362
 
19363
+    flags+=("--allow-wildcard-routes")
19364
+    local_nonpersistent_flags+=("--allow-wildcard-routes")
19365
+    flags+=("--allowed-domains=")
19366
+    local_nonpersistent_flags+=("--allowed-domains=")
19363 19367
     flags+=("--as=")
19364 19368
     local_nonpersistent_flags+=("--as=")
19365 19369
     flags+=("--certificate-authority=")
... ...
@@ -19388,6 +19398,8 @@ _openshift_infra_router()
19388 19388
     local_nonpersistent_flags+=("--default-certificate-dir=")
19389 19389
     flags+=("--default-certificate-path=")
19390 19390
     local_nonpersistent_flags+=("--default-certificate-path=")
19391
+    flags+=("--denied-domains=")
19392
+    local_nonpersistent_flags+=("--denied-domains=")
19391 19393
     flags+=("--extended-validation")
19392 19394
     local_nonpersistent_flags+=("--extended-validation")
19393 19395
     flags+=("--fields=")
... ...
@@ -19386,6 +19386,10 @@ _openshift_infra_f5-router()
19386 19386
     flags_with_completion=()
19387 19387
     flags_completion=()
19388 19388
 
19389
+    flags+=("--allow-wildcard-routes")
19390
+    local_nonpersistent_flags+=("--allow-wildcard-routes")
19391
+    flags+=("--allowed-domains=")
19392
+    local_nonpersistent_flags+=("--allowed-domains=")
19389 19393
     flags+=("--as=")
19390 19394
     local_nonpersistent_flags+=("--as=")
19391 19395
     flags+=("--certificate-authority=")
... ...
@@ -19408,6 +19412,8 @@ _openshift_infra_f5-router()
19408 19408
     local_nonpersistent_flags+=("--config=")
19409 19409
     flags+=("--context=")
19410 19410
     local_nonpersistent_flags+=("--context=")
19411
+    flags+=("--denied-domains=")
19412
+    local_nonpersistent_flags+=("--denied-domains=")
19411 19413
     flags+=("--f5-host=")
19412 19414
     local_nonpersistent_flags+=("--f5-host=")
19413 19415
     flags+=("--f5-http-vserver=")
... ...
@@ -19521,6 +19527,10 @@ _openshift_infra_router()
19521 19521
     flags_with_completion=()
19522 19522
     flags_completion=()
19523 19523
 
19524
+    flags+=("--allow-wildcard-routes")
19525
+    local_nonpersistent_flags+=("--allow-wildcard-routes")
19526
+    flags+=("--allowed-domains=")
19527
+    local_nonpersistent_flags+=("--allowed-domains=")
19524 19528
     flags+=("--as=")
19525 19529
     local_nonpersistent_flags+=("--as=")
19526 19530
     flags+=("--certificate-authority=")
... ...
@@ -19549,6 +19559,8 @@ _openshift_infra_router()
19549 19549
     local_nonpersistent_flags+=("--default-certificate-dir=")
19550 19550
     flags+=("--default-certificate-path=")
19551 19551
     local_nonpersistent_flags+=("--default-certificate-path=")
19552
+    flags+=("--denied-domains=")
19553
+    local_nonpersistent_flags+=("--denied-domains=")
19552 19554
     flags+=("--extended-validation")
19553 19555
     local_nonpersistent_flags+=("--extended-validation")
19554 19556
     flags+=("--fields=")
... ...
@@ -27,6 +27,14 @@ that you must have a cluster\-wide administrative role to view all namespaces.
27 27
 
28 28
 .SH OPTIONS
29 29
 .PP
30
+\fB\-\-allow\-wildcard\-routes\fP=false
31
+    Allow wildcard host names for routes
32
+
33
+.PP
34
+\fB\-\-allowed\-domains\fP=[]
35
+    List of comma separated domains to allow in routes. If specified, only the domains in this list will be allowed routes. Note that domains in the denied list take precedence over the ones in the allowed list
36
+
37
+.PP
30 38
 \fB\-\-api\-version\fP=""
31 39
     DEPRECATED: The API version to use when talking to the server
32 40
 
... ...
@@ -59,6 +67,10 @@ that you must have a cluster\-wide administrative role to view all namespaces.
59 59
     The name of the kubeconfig context to use
60 60
 
61 61
 .PP
62
+\fB\-\-denied\-domains\fP=[]
63
+    List of comma separated domains to deny in routes
64
+
65
+.PP
62 66
 \fB\-\-f5\-host\fP=""
63 67
     The host of F5 BIG\-IP's management interface
64 68
 
... ...
@@ -35,6 +35,14 @@ that you must have a cluster\-wide administrative role to view all namespaces.
35 35
 
36 36
 .SH OPTIONS
37 37
 .PP
38
+\fB\-\-allow\-wildcard\-routes\fP=false
39
+    Allow wildcard host names for routes
40
+
41
+.PP
42
+\fB\-\-allowed\-domains\fP=[]
43
+    List of comma separated domains to allow in routes. If specified, only the domains in this list will be allowed routes. Note that domains in the denied list take precedence over the ones in the allowed list
44
+
45
+.PP
38 46
 \fB\-\-api\-version\fP=""
39 47
     DEPRECATED: The API version to use when talking to the server
40 48
 
... ...
@@ -79,6 +87,10 @@ that you must have a cluster\-wide administrative role to view all namespaces.
79 79
     A path to default certificate to use for routes that don't expose a TLS server cert; in PEM format
80 80
 
81 81
 .PP
82
+\fB\-\-denied\-domains\fP=[]
83
+    List of comma separated domains to deny in routes
84
+
85
+.PP
82 86
 \fB\-\-extended\-validation\fP=true
83 87
     If set, then an additional extended validation step is performed on all routes admitted in by this router. Defaults to true and enables the extended validation checks.
84 88
 
... ...
@@ -16,7 +16,7 @@ RUN INSTALL_PKGS="haproxy" && \
16 16
     yum clean all && \
17 17
     mkdir -p /var/lib/haproxy/router/{certs,cacerts} && \
18 18
     mkdir -p /var/lib/haproxy/{conf,run,bin,log} && \
19
-    touch /var/lib/haproxy/conf/{{os_http_be,os_edge_http_be,os_tcp_be,os_sni_passthrough,os_reencrypt,os_edge_http_expose,os_edge_http_redirect,cert_config}.map,haproxy.config} && \
19
+    touch /var/lib/haproxy/conf/{{os_http_be,os_edge_http_be,os_tcp_be,os_sni_passthrough,os_reencrypt,os_edge_http_expose,os_edge_http_redirect,cert_config,os_wildcard_domain}.map,haproxy.config} && \
20 20
     chmod -R 777 /var && \
21 21
     setcap 'cap_net_bind_service=ep' /usr/sbin/haproxy
22 22
 
... ...
@@ -100,15 +100,36 @@ frontend public
100 100
   acl secure_redirect base,map_beg(/var/lib/haproxy/conf/os_edge_http_redirect.map) -m found
101 101
   redirect scheme https if secure_redirect
102 102
 
103
+{{ if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
104
+  #  Check for wildcard domains with redirected http routes.
105
+  acl wildcard_domain hdr(host),map_reg(/var/lib/haproxy/conf/os_wildcard_domain.map) -m found
106
+
107
+  acl wildcard_secure_redirect base,map_reg(/var/lib/haproxy/conf/os_edge_http_redirect.map) -m found
108
+  redirect scheme https if wildcard_domain wildcard_secure_redirect
109
+
110
+{{ end }}
111
+
103 112
   # Check if it is an edge route exposed insecurely.
104 113
   acl edge_http_expose base,map_beg(/var/lib/haproxy/conf/os_edge_http_expose.map) -m found
105 114
   use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_expose.map)] if edge_http_expose
106 115
 
107 116
   # map to http backend
108 117
   # Search from most specific to general path (host case).
118
+  acl http_backend base,map_beg(/var/lib/haproxy/conf/os_http_be.map) -m found
119
+  use_backend be_http_%[base,map_beg(/var/lib/haproxy/conf/os_http_be.map)] if http_backend
120
+
121
+{{ if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
122
+  #  Check for wildcard domains with exposed http routes.
123
+  acl wildcard_edge_http_expose base,map_reg(/var/lib/haproxy/conf/os_edge_http_expose.map) -m found
124
+  use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_expose.map)] if wildcard_domain wildcard_edge_http_expose
125
+
126
+  # map to http backend
127
+  # Search from most specific to general path (host case).
109 128
   # Note: If no match, haproxy uses the default_backend, no other
110 129
   #       use_backend directives below this will be processed.
111
-  use_backend be_http_%[base,map_beg(/var/lib/haproxy/conf/os_http_be.map)]
130
+  use_backend be_http_%[base,map_reg(/var/lib/haproxy/conf/os_http_be.map)] if wildcard_domain
131
+
132
+{{ end }}
112 133
 
113 134
   default_backend openshift_default
114 135
 
... ...
@@ -125,6 +146,15 @@ frontend public_ssl
125 125
   acl sni_passthrough req.ssl_sni,map(/var/lib/haproxy/conf/os_sni_passthrough.map) -m found
126 126
   use_backend be_tcp_%[req.ssl_sni,map(/var/lib/haproxy/conf/os_tcp_be.map)] if sni sni_passthrough
127 127
 
128
+{{ if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
129
+  #  Check for wildcard domains with passthrough.
130
+  acl sni_wildcard_domain req.ssl_sni,map_reg(/var/lib/haproxy/conf/os_wildcard_domain.map) -m found
131
+
132
+  acl sni_wildcard_passthrough req.ssl_sni,map_reg(/var/lib/haproxy/conf/os_sni_passthrough.map) -m found
133
+  use_backend be_tcp_%[req.ssl_sni,map_reg(/var/lib/haproxy/conf/os_tcp_be.map)] if sni sni_wildcard_domain sni_wildcard_passthrough
134
+
135
+{{ end }}
136
+
128 137
   # if the route is SNI and NOT passthrough enter the termination flow
129 138
   use_backend be_sni if sni
130 139
 
... ...
@@ -162,9 +192,23 @@ frontend fe_sni
162 162
 
163 163
   # map to http backend
164 164
   # Search from most specific to general path (host case).
165
+  acl http_backend base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map) -m found
166
+  use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map)] if http_backend
167
+
168
+{{ if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
169
+  #  Check for wildcard domains with redirected or exposed http routes.
170
+  acl sni_wildcard_domain hdr(host),map_reg(/var/lib/haproxy/conf/os_wildcard_domain.map) -m found
171
+
172
+  acl wildcard_reencrypt base,map_reg(/var/lib/haproxy/conf/os_reencrypt.map) -m found
173
+  use_backend be_secure_%[base,map_reg(/var/lib/haproxy/conf/os_reencrypt.map)] if sni_wildcard_domain wildcard_reencrypt
174
+
175
+  # map to http backend
176
+  # Search from most specific to general path (host case).
165 177
   # Note: If no match, haproxy uses the default_backend, no other
166 178
   #       use_backend directives below this will be processed.
167
-  use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map)]
179
+  use_backend be_edge_http_%[base,map_reg(/var/lib/haproxy/conf/os_edge_http_be.map)] if sni_wildcard_domain
180
+
181
+{{ end }}
168 182
 
169 183
   default_backend openshift_default
170 184
 
... ...
@@ -199,9 +243,22 @@ frontend fe_no_sni
199 199
 
200 200
   # map to http backend
201 201
   # Search from most specific to general path (host case).
202
+  acl edge_http_backend base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map) -m found
203
+  use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map)] if edge_http_backend
204
+
205
+{{ if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
206
+  acl host_wildcard_domain req.ssl_sni,map_reg(/var/lib/haproxy/conf/os_wildcard_domain.map) -m found
207
+
208
+  acl host_reencrypt base,map_reg(/var/lib/haproxy/conf/os_reencrypt.map) -m found
209
+  use_backend be_secure_%[base,map_reg(/var/lib/haproxy/conf/os_reencrypt.map)] if host_wildcard_domain host_reencrypt
210
+
211
+  # map to http backend
212
+  # Search from most specific to general path (host case).
202 213
   # Note: If no match, haproxy uses the default_backend, no other
203 214
   #       use_backend directives below this will be processed.
204
-  use_backend be_edge_http_%[base,map_beg(/var/lib/haproxy/conf/os_edge_http_be.map)]
215
+  use_backend be_edge_http_%[base,map_reg(/var/lib/haproxy/conf/os_edge_http_be.map)] if host_wildcard_domain
216
+
217
+{{ end }}
205 218
 
206 219
   default_backend openshift_default
207 220
 
... ...
@@ -321,8 +378,8 @@ backend be_edge_http_{{$cfgIdx}}
321 321
     {{ end }}{{/* end iterate over services */}}
322 322
   {{ end }}{{/* end if tls==edge/none */}}
323 323
 
324
-# Secure backend, pass through
325 324
   {{ if eq $cfg.TLSTermination "passthrough" }}
325
+# Secure backend, pass through
326 326
 backend be_tcp_{{$cfgIdx}}
327 327
 {{ if ne (env "ROUTER_SYSLOG_ADDRESS" "") ""}}
328 328
   option tcplog
... ...
@@ -385,8 +442,8 @@ backend be_tcp_{{$cfgIdx}}
385 385
     {{ end }}{{/* end iterate over services*/}}
386 386
   {{ end }}{{/*end tls==passthrough*/}}
387 387
 
388
-# Secure backend which requires re-encryption
389 388
   {{ if eq $cfg.TLSTermination "reencrypt" }}
389
+# Secure backend which requires re-encryption
390 390
 backend be_secure_{{$cfgIdx}}
391 391
   mode http
392 392
   option redispatch
... ...
@@ -463,13 +520,34 @@ backend be_secure_{{$cfgIdx}}
463 463
 
464 464
 {{/*--------------------------------- END OF HAPROXY CONFIG, BELOW ARE MAPPING FILES ------------------------*/}}
465 465
 {{/*
466
+    os_wildcard_domain.map: contains a mapping of wildcard hosts for a
467
+			[sub]domain regexps. This map is used to check if
468
+			a host matches a [sub]domain with has wildcard support.
469
+*/}}
470
+{{ define "/var/lib/haproxy/conf/os_wildcard_domain.map" }}
471
+{{     if matchPattern "true|TRUE" (env "ROUTER_ALLOW_WILDCARD_ROUTES" "")}}
472
+{{       range $idx, $cfg := .State }}
473
+{{         if ne $cfg.Host ""}}
474
+{{           if $cfg.IsWildcard }}
475
+{{genDomainWildcardRegexp $cfg.Host "" true}} 1
476
+{{           end }}
477
+{{         end }}
478
+{{       end }}
479
+{{     end }}{{/* end if router allows wildcard routes */}}
480
+{{ end }}{{/* end wildcard domain map template */}}
481
+
482
+{{/*
466 483
     os_http_be.map: contains a mapping of www.example.com -> <service name>.  This map is used to discover the correct backend
467 484
                         by attaching a prefix (be_http_) by use_backend statements if acls are matched.
468 485
 */}}
469 486
 {{ define "/var/lib/haproxy/conf/os_http_be.map" }}
470 487
 {{     range $idx, $cfg := .State }}
471 488
 {{       if and (ne $cfg.Host "") (eq $cfg.TLSTermination "")}}
489
+{{         if $cfg.IsWildcard }}
490
+{{genDomainWildcardRegexp $cfg.Host $cfg.Path false}} {{$idx}}
491
+{{         else }}
472 492
 {{$cfg.Host}}{{$cfg.Path}} {{$idx}}
493
+{{         end }}
473 494
 {{       end }}
474 495
 {{     end }}
475 496
 {{ end }}{{/* end http host map template */}}
... ...
@@ -481,7 +559,11 @@ backend be_secure_{{$cfgIdx}}
481 481
 {{ define "/var/lib/haproxy/conf/os_edge_http_be.map" }}
482 482
 {{     range $idx, $cfg := .State }}
483 483
 {{       if and (ne $cfg.Host "") (eq $cfg.TLSTermination "edge")}}
484
+{{         if $cfg.IsWildcard }}
485
+{{genDomainWildcardRegexp $cfg.Host $cfg.Path false}} {{$idx}}
486
+{{         else }}
484 487
 {{$cfg.Host}}{{$cfg.Path}} {{$idx}}
488
+{{         end }}
485 489
 {{       end }}
486 490
 {{     end }}
487 491
 {{ end }}{{/* end edge http host map template */}}
... ...
@@ -494,7 +576,11 @@ backend be_secure_{{$cfgIdx}}
494 494
 {{ define "/var/lib/haproxy/conf/os_edge_http_expose.map" }}
495 495
 {{     range $idx, $cfg := .State }}
496 496
 {{       if and (ne $cfg.Host "") (and (eq $cfg.TLSTermination "edge") (eq $cfg.InsecureEdgeTerminationPolicy "Allow"))}}
497
+{{         if $cfg.IsWildcard }}
498
+{{genDomainWildcardRegexp $cfg.Host $cfg.Path false}} {{$idx}}
499
+{{         else }}
497 500
 {{$cfg.Host}}{{$cfg.Path}} {{$idx}}
501
+{{         end }}
498 502
 {{       end }}
499 503
 {{     end }}
500 504
 {{ end }}{{/* end edge insecure expose http host map template */}}
... ...
@@ -507,7 +593,11 @@ backend be_secure_{{$cfgIdx}}
507 507
 {{ define "/var/lib/haproxy/conf/os_edge_http_redirect.map" }}
508 508
 {{     range $idx, $cfg := .State }}
509 509
 {{       if and (ne $cfg.Host "") (and (eq $cfg.TLSTermination "edge") (eq $cfg.InsecureEdgeTerminationPolicy "Redirect"))}}
510
+{{         if $cfg.IsWildcard }}
511
+{{genDomainWildcardRegexp $cfg.Host $cfg.Path false}} {{$idx}}
512
+{{         else }}
510 513
 {{$cfg.Host}}{{$cfg.Path}} {{$idx}}
514
+{{         end }}
511 515
 {{       end }}
512 516
 {{     end }}
513 517
 {{ end }}{{/* end edge insecure redirect http host map template */}}
... ...
@@ -520,7 +610,11 @@ backend be_secure_{{$cfgIdx}}
520 520
 {{ define "/var/lib/haproxy/conf/os_tcp_be.map" }}
521 521
 {{     range $idx, $cfg := .State }}
522 522
 {{       if and (eq $cfg.Path "") (and (ne $cfg.Host "") (or (eq $cfg.TLSTermination "passthrough") (eq $cfg.TLSTermination "reencrypt"))) }}
523
+{{         if $cfg.IsWildcard }}
524
+{{genDomainWildcardRegexp $cfg.Host "" true}} {{$idx}}
525
+{{         else }}
523 526
 {{$cfg.Host}} {{$idx}}
527
+{{         end }}
524 528
 {{       end }}
525 529
 {{     end }}
526 530
 {{ end }}{{/* end tcp host map template */}}
... ...
@@ -532,7 +626,11 @@ backend be_secure_{{$cfgIdx}}
532 532
 {{ define "/var/lib/haproxy/conf/os_sni_passthrough.map" }}
533 533
 {{     range $idx, $cfg := .State }}
534 534
 {{       if and (eq $cfg.Path "") (eq $cfg.TLSTermination "passthrough") }}
535
+{{         if $cfg.IsWildcard }}
536
+{{genDomainWildcardRegexp $cfg.Host "" true}} {{$idx}}
537
+{{         else }}
535 538
 {{$cfg.Host}} 1
539
+{{         end }}
536 540
 {{       end }}
537 541
 {{     end }}
538 542
 {{ end }}{{/* end sni passthrough map template */}}
... ...
@@ -545,7 +643,11 @@ backend be_secure_{{$cfgIdx}}
545 545
 {{ define "/var/lib/haproxy/conf/os_reencrypt.map" }}
546 546
 {{     range $idx, $cfg := .State }}
547 547
 {{       if and (ne $cfg.Host "") (eq $cfg.TLSTermination "reencrypt") }}
548
+{{         if $cfg.IsWildcard }}
549
+{{genDomainWildcardRegexp $cfg.Host $cfg.Path false}} {{$idx}}
550
+{{         else }}
548 551
 {{$cfg.Host}}{{$cfg.Path}} {{$idx}}
552
+{{         end }}
549 553
 {{       end }}
550 554
 {{     end }}
551 555
 {{ end }}{{/* end reencrypt map template */}}
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	ocmd "github.com/openshift/origin/pkg/cmd/cli/cmd"
15 15
 	"github.com/openshift/origin/pkg/cmd/util"
16 16
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
17
+	routeapi "github.com/openshift/origin/pkg/route/api"
17 18
 	"github.com/openshift/origin/pkg/router/controller"
18 19
 	f5plugin "github.com/openshift/origin/pkg/router/f5"
19 20
 )
... ...
@@ -154,6 +155,23 @@ func (o *F5RouterOptions) Validate() error {
154 154
 	return o.F5Router.Validate()
155 155
 }
156 156
 
157
+// F5RouteAdmitterFunc returns a func that checks if a route is a
158
+// wildcard route and currently denies it.
159
+func (o *F5RouterOptions) F5RouteAdmitterFunc() controller.RouteAdmissionFunc {
160
+	return func(route *routeapi.Route) error {
161
+		if err := o.AdmissionCheck(route); err != nil {
162
+			return err
163
+		}
164
+
165
+		if _, wildcard := routeapi.NormalizeWildcardHost(route.Spec.Host); wildcard {
166
+			// TODO: F5 wildcard route support.
167
+			return fmt.Errorf("Wildcard routes are currently not supported by the F5 router")
168
+		}
169
+
170
+		return nil
171
+	}
172
+}
173
+
157 174
 // Run launches an F5 route sync process using the provided options. It never exits.
158 175
 func (o *F5RouterOptions) Run() error {
159 176
 	cfg := f5plugin.F5PluginConfig{
... ...
@@ -177,7 +195,8 @@ func (o *F5RouterOptions) Run() error {
177 177
 	}
178 178
 
179 179
 	statusPlugin := controller.NewStatusAdmitter(f5Plugin, oc, o.RouterName)
180
-	plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
180
+	uniqueHostPlugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
181
+	plugin := controller.NewHostAdmitter(uniqueHostPlugin, o.F5RouteAdmitterFunc(), false, statusPlugin)
181 182
 
182 183
 	factory := o.RouterSelection.NewFactory(oc, kc)
183 184
 	controller := factory.Create(plugin)
... ...
@@ -43,6 +43,15 @@ type RouterSelection struct {
43 43
 	ProjectLabels        labels.Selector
44 44
 
45 45
 	IncludeUDP bool
46
+
47
+	DeniedDomains      []string
48
+	BlacklistedDomains sets.String
49
+
50
+	AllowedDomains     []string
51
+	WhitelistedDomains sets.String
52
+
53
+	AllowWildcardRoutes        bool
54
+	RestrictSubdomainOwnership bool
46 55
 }
47 56
 
48 57
 // Bind sets the appropriate labels
... ...
@@ -55,6 +64,9 @@ func (o *RouterSelection) Bind(flag *pflag.FlagSet) {
55 55
 	flag.StringVar(&o.ProjectLabelSelector, "project-labels", cmdutil.Env("PROJECT_LABELS", ""), "A label selector to apply to projects to watch; if '*' watches all projects the client can access")
56 56
 	flag.StringVar(&o.NamespaceLabelSelector, "namespace-labels", cmdutil.Env("NAMESPACE_LABELS", ""), "A label selector to apply to namespaces to watch")
57 57
 	flag.BoolVar(&o.IncludeUDP, "include-udp-endpoints", false, "If true, UDP endpoints will be considered as candidates for routing")
58
+	flag.StringSliceVar(&o.DeniedDomains, "denied-domains", envVarAsStrings("ROUTER_DENIED_DOMAINS", "", ","), "List of comma separated domains to deny in routes")
59
+	flag.StringSliceVar(&o.AllowedDomains, "allowed-domains", envVarAsStrings("ROUTER_ALLOWED_DOMAINS", "", ","), "List of comma separated domains to allow in routes. If specified, only the domains in this list will be allowed routes. Note that domains in the denied list take precedence over the ones in the allowed list")
60
+	flag.BoolVar(&o.AllowWildcardRoutes, "allow-wildcard-routes", cmdutil.Env("ROUTER_ALLOW_WILDCARD_ROUTES", "") == "true", "Allow wildcard host names for routes")
58 61
 }
59 62
 
60 63
 // RouteSelectionFunc returns a func that identifies the host for a route.
... ...
@@ -83,6 +95,50 @@ func (o *RouterSelection) RouteSelectionFunc() controller.RouteHostFunc {
83 83
 	}
84 84
 }
85 85
 
86
+func (o *RouterSelection) AdmissionCheck(route *routeapi.Route) error {
87
+	if len(route.Spec.Host) < 1 {
88
+		return nil
89
+	}
90
+
91
+	if hostInDomainList(route.Spec.Host, o.BlacklistedDomains) {
92
+		glog.V(4).Infof("host %s in list of denied domains", route.Spec.Host)
93
+		return fmt.Errorf("host in list of denied domains")
94
+	}
95
+
96
+	if o.WhitelistedDomains.Len() > 0 {
97
+		glog.V(4).Infof("Checking if host %s is in the list of allowed domains", route.Spec.Host)
98
+		if hostInDomainList(route.Spec.Host, o.WhitelistedDomains) {
99
+			glog.V(4).Infof("host %s admitted - in the list of allowed domains", route.Spec.Host)
100
+			return nil
101
+		}
102
+
103
+		glog.V(4).Infof("host %s rejected - not in the list of allowed domains", route.Spec.Host)
104
+		return fmt.Errorf("host not in the allowed list of domains")
105
+	}
106
+
107
+	glog.V(4).Infof("host %s admitted", route.Spec.Host)
108
+	return nil
109
+}
110
+
111
+// RouteAdmissionFunc returns a func that checks if a route can be admitted
112
+// based on blacklist & whitelist checks and wildcard routes policy setting.
113
+// Note: The blacklist settings trumps the whitelist ones.
114
+func (o *RouterSelection) RouteAdmissionFunc() controller.RouteAdmissionFunc {
115
+	return func(route *routeapi.Route) error {
116
+		if err := o.AdmissionCheck(route); err != nil {
117
+			return err
118
+		}
119
+
120
+		if _, wildcard := routeapi.NormalizeWildcardHost(route.Spec.Host); wildcard {
121
+			if !o.AllowWildcardRoutes {
122
+				return fmt.Errorf("wildcard routes are not allowed")
123
+			}
124
+		}
125
+
126
+		return nil
127
+	}
128
+}
129
+
86 130
 // Complete converts string representations of field and label selectors to their parsed equivalent, or
87 131
 // returns an error.
88 132
 func (o *RouterSelection) Complete() error {
... ...
@@ -138,6 +194,13 @@ func (o *RouterSelection) Complete() error {
138 138
 		}
139 139
 		o.NamespaceLabels = s
140 140
 	}
141
+
142
+	o.BlacklistedDomains = sets.NewString(o.DeniedDomains...)
143
+	o.WhitelistedDomains = sets.NewString(o.AllowedDomains...)
144
+
145
+	// Restrict subdomains is currently enforced for wildcard routes.
146
+	o.RestrictSubdomainOwnership = o.AllowWildcardRoutes
147
+
141 148
 	return nil
142 149
 }
143 150
 
... ...
@@ -198,3 +261,28 @@ func (n namespaceNames) NamespaceNames() (sets.String, error) {
198 198
 	}
199 199
 	return names, nil
200 200
 }
201
+
202
+func envVarAsStrings(name, defaultValue, seperator string) []string {
203
+	strlist := []string{}
204
+	if env := cmdutil.Env(name, defaultValue); env != "" {
205
+		values := strings.Split(env, seperator)
206
+		for i := range values {
207
+			if val := strings.TrimSpace(values[i]); val != "" {
208
+				strlist = append(strlist, val)
209
+			}
210
+		}
211
+	}
212
+	return strlist
213
+}
214
+
215
+func hostInDomainList(host string, domains sets.String) bool {
216
+	if domains.Has(host) {
217
+		return true
218
+	}
219
+
220
+	if idx := strings.IndexRune(host, '.'); idx > 0 {
221
+		return hostInDomainList(host[idx+1:], domains)
222
+	}
223
+
224
+	return false
225
+}
... ...
@@ -207,7 +207,8 @@ func (o *TemplateRouterOptions) Run() error {
207 207
 	if o.ExtendedValidation {
208 208
 		nextPlugin = controller.NewExtendedValidator(nextPlugin, controller.RejectionRecorder(statusPlugin))
209 209
 	}
210
-	plugin := controller.NewUniqueHost(nextPlugin, o.RouteSelectionFunc(), controller.RejectionRecorder(statusPlugin))
210
+	uniqueHostPlugin := controller.NewUniqueHost(nextPlugin, o.RouteSelectionFunc(), controller.RejectionRecorder(statusPlugin))
211
+	plugin := controller.NewHostAdmitter(uniqueHostPlugin, o.RouteAdmissionFunc(), o.RestrictSubdomainOwnership, controller.RejectionRecorder(statusPlugin))
211 212
 
212 213
 	factory := o.RouterSelection.NewFactory(oc, kc)
213 214
 	controller := factory.Create(plugin)
... ...
@@ -1,9 +1,15 @@
1 1
 package api
2 2
 
3 3
 import (
4
+	"strings"
5
+
4 6
 	kapi "k8s.io/kubernetes/pkg/api"
5 7
 )
6 8
 
9
+const (
10
+	RouteWildcardPrefix = "*."
11
+)
12
+
7 13
 // IngressConditionStatus returns the first status and condition matching the provided ingress condition type. Conditions
8 14
 // prefer the first matching entry and clients are allowed to ignore later conditions of the same type.
9 15
 func IngressConditionStatus(ingress *RouteIngress, t RouteIngressConditionType) (kapi.ConditionStatus, RouteIngressCondition) {
... ...
@@ -33,3 +39,16 @@ func RouteLessThan(route1, route2 *Route) bool {
33 33
 
34 34
 	return false
35 35
 }
36
+
37
+// NormalizeWildcardHost tests if a host is wildcarded and returns
38
+// the "normalized" (domain name currently) form of the host.
39
+func NormalizeWildcardHost(host string) (string, bool) {
40
+	if len(host) > 0 {
41
+		if strings.HasPrefix(host, RouteWildcardPrefix) {
42
+			// For wildcard hosts, strip the prefix.
43
+			return host[len(RouteWildcardPrefix):], true
44
+		}
45
+	}
46
+
47
+	return host, false
48
+}
... ...
@@ -88,3 +88,53 @@ func TestRouteLessThan(t *testing.T) {
88 88
 		}
89 89
 	}
90 90
 }
91
+
92
+func TestNormalizeWildcardHost(t *testing.T) {
93
+	tests := []struct {
94
+		name        string
95
+		host        string
96
+		expectation string
97
+		wildcard    bool
98
+	}{
99
+		{
100
+			name:        "plain",
101
+			host:        "www.host.test",
102
+			expectation: "www.host.test",
103
+			wildcard:    false,
104
+		},
105
+		{
106
+			name:        "aceswild",
107
+			host:        "*.aceswild.test",
108
+			expectation: "aceswild.test",
109
+			wildcard:    true,
110
+		},
111
+		{
112
+			name:        "otherwild",
113
+			host:        "aces.*.test",
114
+			expectation: "aces.*.test",
115
+			wildcard:    false,
116
+		},
117
+		{
118
+			name:        "Invalid host",
119
+			host:        "*.aces.*.test",
120
+			expectation: "aces.*.test",
121
+			wildcard:    true,
122
+		},
123
+		{
124
+			name:        "No host",
125
+			host:        "",
126
+			expectation: "",
127
+			wildcard:    false,
128
+		},
129
+	}
130
+
131
+	for _, tc := range tests {
132
+		host, flag := NormalizeWildcardHost(tc.host)
133
+
134
+		if flag != tc.wildcard {
135
+			t.Errorf("Test case %s expected %t got %t", tc.name, tc.wildcard, flag)
136
+		} else if host != tc.expectation {
137
+			t.Errorf("Test case %s expected %v got %v", tc.name, tc.expectation, host)
138
+		}
139
+	}
140
+}
... ...
@@ -26,7 +26,8 @@ func ValidateRoute(route *routeapi.Route) field.ErrorList {
26 26
 
27 27
 	//host is not required but if it is set ensure it meets DNS requirements
28 28
 	if len(route.Spec.Host) > 0 {
29
-		if len(kvalidation.IsDNS1123Subdomain(route.Spec.Host)) != 0 {
29
+		hostname, _ := routeapi.NormalizeWildcardHost(route.Spec.Host)
30
+		if len(kvalidation.IsDNS1123Subdomain(hostname)) != 0 {
30 31
 			result = append(result, field.Invalid(specPath.Child("host"), route.Spec.Host, "host must conform to DNS 952 subdomain conventions"))
31 32
 		}
32 33
 	}
... ...
@@ -182,6 +182,34 @@ func TestValidateRoute(t *testing.T) {
182 182
 			expectedErrors: 1,
183 183
 		},
184 184
 		{
185
+			name: "Wildcard host",
186
+			route: &api.Route{
187
+				ObjectMeta: kapi.ObjectMeta{
188
+					Name:      "name",
189
+					Namespace: "aceswild",
190
+				},
191
+				Spec: api.RouteSpec{
192
+					Host: "*.aceswild.com",
193
+					To:   createRouteSpecTo("serviceName", "Service"),
194
+				},
195
+			},
196
+			expectedErrors: 0,
197
+		},
198
+		{
199
+			name: "Invalid Wildcard host",
200
+			route: &api.Route{
201
+				ObjectMeta: kapi.ObjectMeta{
202
+					Name:      "name",
203
+					Namespace: "wildly",
204
+				},
205
+				Spec: api.RouteSpec{
206
+					Host: "*.not.*.wild.ly",
207
+					To:   createRouteSpecTo("serviceName", "Service"),
208
+				},
209
+			},
210
+			expectedErrors: 1,
211
+		},
212
+		{
185 213
 			name: "No service name",
186 214
 			route: &api.Route{
187 215
 				ObjectMeta: kapi.ObjectMeta{
... ...
@@ -997,3 +1025,146 @@ func TestExtendedValidateRoute(t *testing.T) {
997 997
 		}
998 998
 	}
999 999
 }
1000
+
1001
+func TestValidateRouteWildcard(t *testing.T) {
1002
+	tests := []struct {
1003
+		name             string
1004
+		route            *api.Route
1005
+		errorExpectation bool
1006
+	}{
1007
+		{
1008
+			name: "No Name",
1009
+			route: &api.Route{
1010
+				ObjectMeta: kapi.ObjectMeta{
1011
+					Namespace: "foo",
1012
+				},
1013
+				Spec: api.RouteSpec{
1014
+					Host: "host",
1015
+					To:   createRouteSpecTo("serviceName", "Service"),
1016
+				},
1017
+			},
1018
+			errorExpectation: false,
1019
+		},
1020
+		{
1021
+			name: "Named host",
1022
+			route: &api.Route{
1023
+				ObjectMeta: kapi.ObjectMeta{
1024
+					Name: "named",
1025
+				},
1026
+				Spec: api.RouteSpec{
1027
+					Host: "www.name.test",
1028
+					To:   createRouteSpecTo("serviceName", "Service"),
1029
+				},
1030
+			},
1031
+			errorExpectation: false,
1032
+		},
1033
+		{
1034
+			name: "aceswild",
1035
+			route: &api.Route{
1036
+				ObjectMeta: kapi.ObjectMeta{
1037
+					Name: "aceswild",
1038
+				},
1039
+				Spec: api.RouteSpec{
1040
+					Host: "*.aceswild.test",
1041
+					To:   createRouteSpecTo("serviceName", "Service"),
1042
+				},
1043
+			},
1044
+			errorExpectation: false,
1045
+		},
1046
+		{
1047
+			name: "another wild",
1048
+			route: &api.Route{
1049
+				ObjectMeta: kapi.ObjectMeta{
1050
+					Name: "anotherwild",
1051
+				},
1052
+				Spec: api.RouteSpec{
1053
+					Host: "*.where.the.wild.things.ar",
1054
+					To:   createRouteSpecTo("serviceName", "Service"),
1055
+				},
1056
+			},
1057
+			errorExpectation: false,
1058
+		},
1059
+		{
1060
+			name: "Invalid host",
1061
+			route: &api.Route{
1062
+				ObjectMeta: kapi.ObjectMeta{
1063
+					Name:      "invalid",
1064
+					Namespace: "foo",
1065
+				},
1066
+				Spec: api.RouteSpec{
1067
+					Host: "aces.*.test",
1068
+					To:   createRouteSpecTo("serviceName", "Service"),
1069
+				},
1070
+			},
1071
+			errorExpectation: true,
1072
+		},
1073
+		{
1074
+			name: "Bad wildcard host",
1075
+			route: &api.Route{
1076
+				ObjectMeta: kapi.ObjectMeta{
1077
+					Name:      "badwildcard",
1078
+					Namespace: "foo",
1079
+				},
1080
+				Spec: api.RouteSpec{
1081
+					Host: "*.aces.*.test",
1082
+					To:   createRouteSpecTo("serviceName", "Service"),
1083
+				},
1084
+			},
1085
+			errorExpectation: true,
1086
+		},
1087
+		{
1088
+			name: "Another bad wildcard host",
1089
+			route: &api.Route{
1090
+				ObjectMeta: kapi.ObjectMeta{
1091
+					Name:      "badwildcard2",
1092
+					Namespace: "foo",
1093
+				},
1094
+				Spec: api.RouteSpec{
1095
+					Host: "*aces.wild.test",
1096
+					To:   createRouteSpecTo("serviceName", "Service"),
1097
+				},
1098
+			},
1099
+			errorExpectation: true,
1100
+		},
1101
+		{
1102
+			name: "Yet another bad wildcard host",
1103
+			route: &api.Route{
1104
+				ObjectMeta: kapi.ObjectMeta{
1105
+					Name:      "badwildcard3",
1106
+					Namespace: "foo",
1107
+				},
1108
+				Spec: api.RouteSpec{
1109
+					Host: "aces*.wild.test",
1110
+					To:   createRouteSpecTo("serviceName", "Service"),
1111
+				},
1112
+			},
1113
+			errorExpectation: true,
1114
+		},
1115
+		{
1116
+			name: "And one more bad wildcard host",
1117
+			route: &api.Route{
1118
+				ObjectMeta: kapi.ObjectMeta{
1119
+					Name:      "badwildcard4",
1120
+					Namespace: "foo",
1121
+				},
1122
+				Spec: api.RouteSpec{
1123
+					Host: "a*es.wild.test",
1124
+					To:   createRouteSpecTo("serviceName", "Service"),
1125
+				},
1126
+			},
1127
+			errorExpectation: true,
1128
+		},
1129
+	}
1130
+
1131
+	for _, tc := range tests {
1132
+		errs := ValidateRoute(tc.route)
1133
+
1134
+		if tc.errorExpectation {
1135
+			if len(errs) == 0 {
1136
+				t.Errorf("Test case %s expected error(s), got none.", tc.name)
1137
+			}
1138
+		} else if len(errs) > 1 {
1139
+			t.Errorf("Test case %s expected no error(s), got %d: %v", tc.name, len(errs), errs)
1140
+		}
1141
+	}
1142
+}
1000 1143
new file mode 100644
... ...
@@ -0,0 +1,204 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+
6
+	"github.com/golang/glog"
7
+	kapi "k8s.io/kubernetes/pkg/api"
8
+	"k8s.io/kubernetes/pkg/util/sets"
9
+	"k8s.io/kubernetes/pkg/watch"
10
+
11
+	routeapi "github.com/openshift/origin/pkg/route/api"
12
+	"github.com/openshift/origin/pkg/router"
13
+)
14
+
15
+// RouteAdmissionFunc determines whether or not to admit a route.
16
+type RouteAdmissionFunc func(*routeapi.Route) error
17
+
18
+// SubdomainToRouteMap contains all routes associated with a subdomain -
19
+// fully qualified and wildcard routes.
20
+type SubdomainToRouteMap map[string][]*routeapi.Route
21
+
22
+// RemoveRoute removes any existing route(s) for a subdomain.
23
+func (srm SubdomainToRouteMap) RemoveRoute(key string, route *routeapi.Route) bool {
24
+	k := 0
25
+	removed := false
26
+
27
+	m := srm[key]
28
+	for i, v := range m {
29
+		if m[i].Namespace == route.Namespace && m[i].Name == route.Name {
30
+			removed = true
31
+		} else {
32
+			m[k] = v
33
+			k++
34
+		}
35
+	}
36
+
37
+	// set the slice length to the final size.
38
+	m = m[:k]
39
+
40
+	if len(m) > 0 {
41
+		srm[key] = m
42
+	} else {
43
+		delete(srm, key)
44
+	}
45
+
46
+	return removed
47
+}
48
+
49
+func (srm SubdomainToRouteMap) InsertRoute(key string, route *routeapi.Route) {
50
+	// To replace any existing route[s], first we remove all old entries.
51
+	srm.RemoveRoute(key, route)
52
+
53
+	m := srm[key]
54
+	for idx := range m {
55
+		if routeapi.RouteLessThan(route, m[idx]) {
56
+			m = append(m, &routeapi.Route{})
57
+			// From: https://github.com/golang/go/wiki/SliceTricks
58
+			copy(m[idx+1:], m[idx:])
59
+			m[idx] = route
60
+			srm[key] = m
61
+
62
+			// Ensure we return from here as we change the iterator.
63
+			return
64
+		}
65
+	}
66
+
67
+	// Newest route or empty slice, add to the end.
68
+	srm[key] = append(m, route)
69
+}
70
+
71
+// HostAdmitter implements the router.Plugin interface to add admission
72
+// control checks for routes in template based, backend-agnostic routers.
73
+type HostAdmitter struct {
74
+	// plugin is the next plugin in the chain.
75
+	plugin router.Plugin
76
+
77
+	// admitter is a route admission function used to determine whether
78
+	// or not to admit routes.
79
+	admitter RouteAdmissionFunc
80
+
81
+	// recorder is an interface for indicating route rejections.
82
+	recorder RejectionRecorder
83
+
84
+	// restrictOwnership adds admission checks to restrict ownership
85
+	// (of subdomains) to a single owner/namespace.
86
+	restrictOwnership bool
87
+
88
+	// subdomainToRoute contains all routes associated with a subdomain
89
+	// (includes fully qualified and wildcard routes).
90
+	subdomainToRoute SubdomainToRouteMap
91
+}
92
+
93
+// NewHostAdmitter creates a plugin wrapper that checks whether or not to
94
+// admit routes and relay them to the next plugin in the chain.
95
+// Recorder is an interface for indicating why a route was rejected.
96
+func NewHostAdmitter(plugin router.Plugin, fn RouteAdmissionFunc, restrict bool, recorder RejectionRecorder) *HostAdmitter {
97
+	return &HostAdmitter{
98
+		plugin:   plugin,
99
+		admitter: fn,
100
+		recorder: recorder,
101
+
102
+		restrictOwnership: restrict,
103
+		subdomainToRoute:  make(SubdomainToRouteMap),
104
+	}
105
+}
106
+
107
+// HandleEndpoints processes watch events on the Endpoints resource.
108
+func (p *HostAdmitter) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error {
109
+	return p.plugin.HandleEndpoints(eventType, endpoints)
110
+}
111
+
112
+// HandleRoute processes watch events on the Route resource.
113
+func (p *HostAdmitter) HandleRoute(eventType watch.EventType, route *routeapi.Route) error {
114
+	if err := p.admitter(route); err != nil {
115
+		glog.Errorf("Route %s not admitted: %s", routeNameKey(route), err.Error())
116
+		p.recorder.RecordRouteRejection(route, "RouteNotAdmitted", err.Error())
117
+		return err
118
+	}
119
+
120
+	if p.restrictOwnership && len(route.Spec.Host) > 0 {
121
+		switch eventType {
122
+		case watch.Added, watch.Modified:
123
+			if err := p.addRoute(route); err != nil {
124
+				glog.Errorf("Route %s not admitted: %s", routeNameKey(route), err.Error())
125
+				p.recorder.RecordRouteRejection(route, "SubdomainAlreadyClaimed", err.Error())
126
+				return err
127
+			}
128
+
129
+		case watch.Deleted:
130
+			if subdomain := getSubdomain(route.Spec.Host); len(subdomain) > 0 {
131
+				p.subdomainToRoute.RemoveRoute(subdomain, route)
132
+			}
133
+		}
134
+	}
135
+
136
+	return p.plugin.HandleRoute(eventType, route)
137
+}
138
+
139
+// HandleAllowedNamespaces limits the scope of valid routes to only those that match
140
+// the provided namespace list.
141
+func (p *HostAdmitter) HandleNamespaces(namespaces sets.String) error {
142
+	return p.plugin.HandleNamespaces(namespaces)
143
+}
144
+
145
+func (p *HostAdmitter) SetLastSyncProcessed(processed bool) error {
146
+	return p.plugin.SetLastSyncProcessed(processed)
147
+}
148
+
149
+// addRoute admits routes based on subdomain ownership - returns errors if the route is not admitted.
150
+func (p *HostAdmitter) addRoute(route *routeapi.Route) error {
151
+	subdomain := getSubdomain(route.Spec.Host)
152
+	if len(subdomain) == 0 {
153
+		return nil
154
+	}
155
+
156
+	routeList, ok := p.subdomainToRoute[subdomain]
157
+	if !ok {
158
+		p.subdomainToRoute.InsertRoute(subdomain, route)
159
+		return nil
160
+	}
161
+
162
+	oldest := routeList[0]
163
+	if oldest.Namespace == route.Namespace {
164
+		p.subdomainToRoute.InsertRoute(subdomain, route)
165
+		return nil
166
+	}
167
+
168
+	// Route is in another namespace, land grab check here.
169
+	if routeapi.RouteLessThan(oldest, route) {
170
+		glog.V(4).Infof("Route %s cannot take subdomain %s from %s", routeNameKey(route), subdomain, routeNameKey(oldest))
171
+		err := fmt.Errorf("a route in another namespace holds subdomain %s and is older than %s", subdomain, route.Name)
172
+		p.recorder.RecordRouteRejection(route, "SubdomainAlreadyClaimed", err.Error())
173
+		return err
174
+	}
175
+
176
+	// Namespace of this route is now the proud owner of the subdomain.
177
+	glog.V(4).Infof("Route %s is reclaiming subdomain %s from namespace %s", routeNameKey(route), subdomain, oldest.Namespace)
178
+
179
+	// Delete all the routes belonging to the previous "owner" (namespace).
180
+	for idx := range routeList {
181
+		msg := fmt.Sprintf("a route in another namespace %s owns subdomain %s", route.Namespace, subdomain)
182
+		glog.V(4).Infof("Route %s not admitted: %s", routeNameKey(routeList[idx]), msg)
183
+		p.recorder.RecordRouteRejection(routeList[idx], "SubdomainAlreadyClaimed", msg)
184
+		p.plugin.HandleRoute(watch.Deleted, routeList[idx])
185
+	}
186
+
187
+	// And claim the subdomain.
188
+	p.subdomainToRoute[subdomain] = []*routeapi.Route{route}
189
+	return nil
190
+}
191
+
192
+func getSubdomain(host string) string {
193
+	if len(host) < 1 {
194
+		return host
195
+	}
196
+
197
+	parts := strings.SplitAfterN(host, ".", 2)
198
+	if len(parts) < 2 {
199
+		return ""
200
+	}
201
+
202
+	return parts[1]
203
+}
0 204
new file mode 100644
... ...
@@ -0,0 +1,317 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+	"testing"
6
+	"time"
7
+
8
+	kapi "k8s.io/kubernetes/pkg/api"
9
+	"k8s.io/kubernetes/pkg/api/unversioned"
10
+	"k8s.io/kubernetes/pkg/watch"
11
+
12
+	routeapi "github.com/openshift/origin/pkg/route/api"
13
+)
14
+
15
+const (
16
+	BlockedTestDomain = "domain.blocked.test"
17
+)
18
+
19
+type rejectionRecorder struct {
20
+	rejections map[string]string
21
+}
22
+
23
+func (_ rejectionRecorder) rejectionKey(route *routeapi.Route) string {
24
+	return route.Namespace + "-" + route.Name
25
+}
26
+
27
+func (r rejectionRecorder) RecordRouteRejection(route *routeapi.Route, reason, message string) {
28
+	r.rejections[r.rejectionKey(route)] = reason
29
+}
30
+
31
+func wildcardAdmitter(route *routeapi.Route) error {
32
+	if len(route.Spec.Host) < 1 {
33
+		return nil
34
+	}
35
+
36
+	if strings.HasSuffix(route.Spec.Host, "."+BlockedTestDomain) {
37
+		return fmt.Errorf("host is not allowed")
38
+	}
39
+
40
+	return nil
41
+}
42
+
43
+func wildcardRejecter(route *routeapi.Route) error {
44
+	if len(route.Spec.Host) < 1 {
45
+		return nil
46
+	}
47
+
48
+	if strings.HasSuffix(route.Spec.Host, "."+BlockedTestDomain) {
49
+		return fmt.Errorf("host is not allowed")
50
+	}
51
+
52
+	_, wildcard := routeapi.NormalizeWildcardHost(route.Spec.Host)
53
+	if wildcard {
54
+		return fmt.Errorf("wildcards not admitted test")
55
+	}
56
+
57
+	return nil
58
+}
59
+
60
+func TestHostAdmit(t *testing.T) {
61
+	p := &fakePlugin{}
62
+	admitter := NewHostAdmitter(p, wildcardAdmitter, true, LogRejections)
63
+	tests := []struct {
64
+		name   string
65
+		host   string
66
+		errors bool
67
+	}{
68
+		{
69
+			name:   "nohost",
70
+			errors: false,
71
+		},
72
+		{
73
+			name:   "allowed",
74
+			host:   "www.host.admission.test",
75
+			errors: false,
76
+		},
77
+		{
78
+			name:   "blocked",
79
+			host:   "www." + BlockedTestDomain,
80
+			errors: true,
81
+		},
82
+		{
83
+			name:   "wildcard",
84
+			host:   "*.aces.wild.test",
85
+			errors: false,
86
+		},
87
+		{
88
+			name:   "blockedwildcard",
89
+			host:   "*." + BlockedTestDomain,
90
+			errors: true,
91
+		},
92
+	}
93
+
94
+	for _, tc := range tests {
95
+		route := &routeapi.Route{
96
+			ObjectMeta: kapi.ObjectMeta{
97
+				Name:      tc.name,
98
+				Namespace: "allow",
99
+			},
100
+			Spec: routeapi.RouteSpec{Host: tc.host},
101
+		}
102
+
103
+		err := admitter.HandleRoute(watch.Added, route)
104
+		if tc.errors {
105
+			if err == nil {
106
+				t.Fatalf("Test case %s expected errors, got none", tc.name)
107
+			}
108
+		} else {
109
+			if err != nil {
110
+				t.Fatalf("Test case %s expected no errors, got %v", tc.name, err)
111
+			}
112
+		}
113
+	}
114
+}
115
+
116
+func TestWildcardHostDeny(t *testing.T) {
117
+	p := &fakePlugin{}
118
+	admitter := NewHostAdmitter(p, wildcardRejecter, false, LogRejections)
119
+	tests := []struct {
120
+		name   string
121
+		host   string
122
+		errors bool
123
+	}{
124
+		{
125
+			name:   "nohost",
126
+			errors: false,
127
+		},
128
+		{
129
+			name:   "allowed",
130
+			host:   "www.host.admission.test",
131
+			errors: false,
132
+		},
133
+		{
134
+			name:   "blocked",
135
+			host:   "www.wildcard." + BlockedTestDomain,
136
+			errors: true,
137
+		},
138
+		{
139
+			name:   "wildcard",
140
+			host:   "*.aces.wild.test",
141
+			errors: true,
142
+		},
143
+		{
144
+			name:   "blockedwildcard",
145
+			host:   "*.wildcard." + BlockedTestDomain,
146
+			errors: true,
147
+		},
148
+		{
149
+			name:   "anotherblockedwildcard",
150
+			host:   "api.wildcard." + BlockedTestDomain,
151
+			errors: true,
152
+		},
153
+	}
154
+
155
+	for _, tc := range tests {
156
+		route := &routeapi.Route{
157
+			ObjectMeta: kapi.ObjectMeta{
158
+				Name:      tc.name,
159
+				Namespace: "deny",
160
+			},
161
+			Spec: routeapi.RouteSpec{Host: tc.host},
162
+		}
163
+
164
+		err := admitter.HandleRoute(watch.Added, route)
165
+		if tc.errors {
166
+			if err == nil {
167
+				t.Fatalf("Test case %s expected errors, got none", tc.name)
168
+			}
169
+		} else {
170
+			if err != nil {
171
+				t.Fatalf("Test case %s expected no errors, got %v", tc.name, err)
172
+			}
173
+		}
174
+	}
175
+}
176
+
177
+func TestWildcardSubDomainOwnership(t *testing.T) {
178
+	p := &fakePlugin{}
179
+
180
+	recorder := rejectionRecorder{rejections: make(map[string]string)}
181
+	admitter := NewHostAdmitter(p, wildcardAdmitter, true, recorder)
182
+
183
+	oldest := unversioned.Time{Time: time.Now()}
184
+
185
+	ownerRoute := &routeapi.Route{
186
+		ObjectMeta: kapi.ObjectMeta{
187
+			CreationTimestamp: oldest,
188
+			Name:              "first",
189
+			Namespace:         "owner",
190
+		},
191
+		Spec: routeapi.RouteSpec{
192
+			Host: "owner.namespace.test",
193
+		},
194
+	}
195
+
196
+	err := admitter.HandleRoute(watch.Added, ownerRoute)
197
+	if err != nil {
198
+		t.Fatalf("Owner route not admitted: %v", err)
199
+	}
200
+
201
+	tests := []struct {
202
+		createdAt unversioned.Time
203
+		name      string
204
+		namespace string
205
+		host      string
206
+		reason    string
207
+	}{
208
+		{
209
+			name:      "nohost",
210
+			namespace: "something",
211
+		},
212
+		{
213
+			name:      "blockedhost",
214
+			namespace: "blocked",
215
+			host:      "www.wildcard." + BlockedTestDomain,
216
+			reason:    "RouteNotAdmitted",
217
+		},
218
+		{
219
+			createdAt: unversioned.Time{Time: oldest.Add(2 * time.Hour)},
220
+			name:      "diffnamespace",
221
+			namespace: "notowner",
222
+			host:      "www.namespace.test",
223
+			reason:    "SubdomainAlreadyClaimed",
224
+		},
225
+		{
226
+			createdAt: unversioned.Time{Time: oldest.Add(2 * time.Hour)},
227
+			name:      "diffns2",
228
+			namespace: "fortytwo",
229
+			host:      "www.namespace.test",
230
+			reason:    "SubdomainAlreadyClaimed",
231
+		},
232
+		{
233
+			createdAt: unversioned.Time{Time: oldest.Add(3 * time.Hour)},
234
+			name:      "host2diffns2",
235
+			namespace: "fortytwo",
236
+			host:      "api.namespace.test",
237
+			reason:    "SubdomainAlreadyClaimed",
238
+		},
239
+		{
240
+			createdAt: unversioned.Time{Time: oldest.Add(4 * time.Hour)},
241
+			name:      "ownernshost",
242
+			namespace: "owner",
243
+			host:      "api.namespace.test",
244
+		},
245
+	}
246
+
247
+	for _, tc := range tests {
248
+		route := &routeapi.Route{
249
+			ObjectMeta: kapi.ObjectMeta{
250
+				CreationTimestamp: tc.createdAt,
251
+				Name:              tc.name,
252
+				Namespace:         tc.namespace,
253
+			},
254
+			Spec: routeapi.RouteSpec{Host: tc.host},
255
+		}
256
+
257
+		err := admitter.HandleRoute(watch.Added, route)
258
+		if tc.reason != "" {
259
+			if err == nil {
260
+				t.Fatalf("Test case %s expected errors, got none", tc.name)
261
+			}
262
+
263
+			k := recorder.rejectionKey(route)
264
+			if recorder.rejections[k] != tc.reason {
265
+				t.Fatalf("Test case %s expected error %s, got %s", tc.name, tc.reason, recorder.rejections[k])
266
+			}
267
+		} else {
268
+			if err != nil {
269
+				t.Fatalf("Test case %s expected no errors, got %v", tc.name, err)
270
+			}
271
+		}
272
+	}
273
+
274
+	wildcardRoute := &routeapi.Route{
275
+		ObjectMeta: kapi.ObjectMeta{
276
+			CreationTimestamp: unversioned.Time{Time: oldest.Add(time.Hour)},
277
+			Name:              "wildcard-owner",
278
+			Namespace:         "owner",
279
+		},
280
+		Spec: routeapi.RouteSpec{
281
+			Host: "*.namespace.test",
282
+		},
283
+	}
284
+
285
+	err = admitter.HandleRoute(watch.Added, wildcardRoute)
286
+	if err != nil {
287
+		t.Fatalf("Wildcard route not admitted: %v", err)
288
+	}
289
+
290
+	// bounce all the routes from the namespace "owner" and claim
291
+	// ownership of the subdomain for the namespace "bouncer".
292
+	bouncer := &routeapi.Route{
293
+		ObjectMeta: kapi.ObjectMeta{
294
+			CreationTimestamp: unversioned.Time{Time: oldest.Add(-1 * time.Hour)},
295
+			Name:              "hosted",
296
+			Namespace:         "bouncer",
297
+		},
298
+		Spec: routeapi.RouteSpec{
299
+			Host: "api.namespace.test",
300
+		},
301
+	}
302
+
303
+	err = admitter.HandleRoute(watch.Added, bouncer)
304
+	if err != nil {
305
+		t.Fatalf("bouncer route expected no errors, got %v", err)
306
+	}
307
+
308
+	// The bouncer route should kick out the owner and wildcard routes.
309
+	bouncedRoutes := []*routeapi.Route{ownerRoute, wildcardRoute}
310
+	for _, route := range bouncedRoutes {
311
+		k := recorder.rejectionKey(route)
312
+		if recorder.rejections[k] != "SubdomainAlreadyClaimed" {
313
+			t.Fatalf("bounced route %s expected a subdomain already claimed error, got %s", k, recorder.rejections[k])
314
+		}
315
+	}
316
+}
... ...
@@ -107,6 +107,8 @@ func NewTemplatePlugin(cfg TemplatePluginConfig, lookupSvc ServiceLookup) (*Temp
107 107
 		"matchPattern":      matchPattern,      //anchors provided regular expression and evaluates against given string
108 108
 		"isInteger":         isInteger,         //determines if a given variable is an integer
109 109
 		"matchValues":       matchValues,       //compares a given string to a list of allowed strings
110
+
111
+		"genDomainWildcardRegexp": genDomainWildcardRegexp, //generates a regular expression matching wildcard hosts (and paths) for a [sub]domain
110 112
 	}
111 113
 	masterTemplate, err := template.New("config").Funcs(globalFuncs).ParseFiles(cfg.TemplatePath)
112 114
 	if err != nil {
... ...
@@ -203,6 +203,23 @@ func matchPattern(pattern, s string) bool {
203 203
 	return false
204 204
 }
205 205
 
206
+// Generate a regular expression to match wildcard hosts (and paths if any)
207
+// for a [sub]domain.
208
+func genDomainWildcardRegexp(hostname, path string, exactPath bool) string {
209
+	route := &routeapi.Route{Spec: routeapi.RouteSpec{Host: hostname}}
210
+	host, wildcard := routeapi.NormalizeWildcardHost(route.Spec.Host)
211
+	if !wildcard {
212
+		return fmt.Sprintf("%s%s", host, path)
213
+	}
214
+
215
+	expr := regexp.QuoteMeta(fmt.Sprintf(".%s%s", host, path))
216
+	if exactPath {
217
+		return fmt.Sprintf("[^\\.]*%s", expr)
218
+	}
219
+
220
+	return fmt.Sprintf("[^\\.]*%s(|/.*)", expr)
221
+}
222
+
206 223
 func endpointsForAlias(alias ServiceAliasConfig, svc ServiceUnit) []Endpoint {
207 224
 	if len(alias.PreferPort) == 0 {
208 225
 		return svc.EndpointTable
... ...
@@ -514,6 +531,7 @@ func (r *templateRouter) routeKey(route *routeapi.Route) string {
514 514
 func (r *templateRouter) AddRoute(serviceID string, weight int32, route *routeapi.Route, host string) bool {
515 515
 	backendKey := r.routeKey(route)
516 516
 
517
+	_, wildcard := routeapi.NormalizeWildcardHost(route.Spec.Host)
517 518
 	config, ok := r.state[backendKey]
518 519
 
519 520
 	if !ok {
... ...
@@ -522,6 +540,7 @@ func (r *templateRouter) AddRoute(serviceID string, weight int32, route *routeap
522 522
 			Namespace:        route.Namespace,
523 523
 			Host:             host,
524 524
 			Path:             route.Spec.Path,
525
+			IsWildcard:       wildcard,
525 526
 			Annotations:      route.Annotations,
526 527
 			ServiceUnitNames: make(map[string]int32),
527 528
 		}
... ...
@@ -43,6 +43,9 @@ type ServiceAliasConfig struct {
43 43
 	// Hash of the route name - used to obscure cookieId
44 44
 	RoutingKeyName string
45 45
 
46
+	// IsWildcard indicates this service unit needs wildcarding support.
47
+	IsWildcard bool
48
+
46 49
 	// Annotations attached to this route
47 50
 	Annotations map[string]string
48 51