Browse code

Have routers take ownership of routes

When a router sees a route, it should make a decision about the name it
wishes to assign to that route (based on its override-hostname or own
settings) and then write the decision back to the route status with an
effective host value (so clients can see what the actual route value
is). If multiple routers attempt to make conflicting writes, have them
remember the conflict so as to avoid battles as they race to the new
value. Errors due to route uniqueness are also written back to the
status.

Each route has an array of RouteIngress structs, which contain an array
of conditions, the routerName (passed via --name to the router), and the
effective host.

Add printers and describers, make ingress empty be a special case.

Clayton Coleman authored on 2015/09/09 05:49:34
Showing 26 changed files
... ...
@@ -21971,7 +21971,67 @@
21971 21971
    },
21972 21972
    "v1.RouteStatus": {
21973 21973
     "id": "v1.RouteStatus",
21974
-    "properties": {}
21974
+    "required": [
21975
+     "ingress"
21976
+    ],
21977
+    "properties": {
21978
+     "ingress": {
21979
+      "type": "array",
21980
+      "items": {
21981
+       "$ref": "v1.RouteIngress"
21982
+      },
21983
+      "description": "traffic reaches this route via these ingress paths"
21984
+     }
21985
+    }
21986
+   },
21987
+   "v1.RouteIngress": {
21988
+    "id": "v1.RouteIngress",
21989
+    "properties": {
21990
+     "host": {
21991
+      "type": "string",
21992
+      "description": "the host name this route is exposed to by the specified router"
21993
+     },
21994
+     "routerName": {
21995
+      "type": "string",
21996
+      "description": "the name of the router exposing this route"
21997
+     },
21998
+     "conditions": {
21999
+      "type": "array",
22000
+      "items": {
22001
+       "$ref": "v1.RouteIngressCondition"
22002
+      },
22003
+      "description": "the conditions that apply to this router"
22004
+     }
22005
+    }
22006
+   },
22007
+   "v1.RouteIngressCondition": {
22008
+    "id": "v1.RouteIngressCondition",
22009
+    "required": [
22010
+     "type",
22011
+     "status"
22012
+    ],
22013
+    "properties": {
22014
+     "type": {
22015
+      "type": "string",
22016
+      "description": "the type of the condition"
22017
+     },
22018
+     "status": {
22019
+      "type": "string",
22020
+      "description": "status is the status of the condition; True, False, or Unknown"
22021
+     },
22022
+     "reason": {
22023
+      "type": "string",
22024
+      "description": "brief reason for the condition's last transition, machine readable constant"
22025
+     },
22026
+     "message": {
22027
+      "type": "string",
22028
+      "description": "human readable message indicating details about this condition"
22029
+     },
22030
+     "lastTransitionTime": {
22031
+      "type": "string",
22032
+      "description": "the last time at which this condition transitioned to the current status"
22033
+     }
22034
+    }
21975 22035
    },
21976 22036
    "v1.SubjectAccessReview": {
21977 22037
     "id": "v1.SubjectAccessReview",
... ...
@@ -17675,6 +17675,7 @@ _openshift_infra_router()
17675 17675
     flags+=("--kubernetes=")
17676 17676
     flags+=("--labels=")
17677 17677
     flags+=("--master=")
17678
+    flags+=("--name=")
17678 17679
     flags+=("--namespace=")
17679 17680
     two_word_flags+=("-n")
17680 17681
     flags+=("--namespace-labels=")
... ...
@@ -17755,6 +17756,7 @@ _openshift_infra_f5-router()
17755 17755
     flags+=("--kubernetes=")
17756 17756
     flags+=("--labels=")
17757 17757
     flags+=("--master=")
17758
+    flags+=("--name=")
17758 17759
     flags+=("--namespace=")
17759 17760
     two_word_flags+=("-n")
17760 17761
     flags+=("--namespace-labels=")
... ...
@@ -197,6 +197,8 @@ v1.routelist
197 197
 v1.routeport
198 198
 v1.routespec
199 199
 v1.routestatus
200
+v1.routeingress
201
+v1.routeingresscondition
200 202
 v1.runasuserstrategyoptions
201 203
 v1.secret
202 204
 v1.secretlist
... ...
@@ -2717,6 +2717,39 @@ func deepCopy_api_Route(in routeapi.Route, out *routeapi.Route, c *conversion.Cl
2717 2717
 	return nil
2718 2718
 }
2719 2719
 
2720
+func deepCopy_api_RouteIngress(in routeapi.RouteIngress, out *routeapi.RouteIngress, c *conversion.Cloner) error {
2721
+	out.Host = in.Host
2722
+	out.RouterName = in.RouterName
2723
+	if in.Conditions != nil {
2724
+		out.Conditions = make([]routeapi.RouteIngressCondition, len(in.Conditions))
2725
+		for i := range in.Conditions {
2726
+			if err := deepCopy_api_RouteIngressCondition(in.Conditions[i], &out.Conditions[i], c); err != nil {
2727
+				return err
2728
+			}
2729
+		}
2730
+	} else {
2731
+		out.Conditions = nil
2732
+	}
2733
+	return nil
2734
+}
2735
+
2736
+func deepCopy_api_RouteIngressCondition(in routeapi.RouteIngressCondition, out *routeapi.RouteIngressCondition, c *conversion.Cloner) error {
2737
+	out.Type = in.Type
2738
+	out.Status = in.Status
2739
+	out.Reason = in.Reason
2740
+	out.Message = in.Message
2741
+	if in.LastTransitionTime != nil {
2742
+		if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil {
2743
+			return err
2744
+		} else {
2745
+			out.LastTransitionTime = newVal.(*unversioned.Time)
2746
+		}
2747
+	} else {
2748
+		out.LastTransitionTime = nil
2749
+	}
2750
+	return nil
2751
+}
2752
+
2720 2753
 func deepCopy_api_RouteList(in routeapi.RouteList, out *routeapi.RouteList, c *conversion.Cloner) error {
2721 2754
 	if newVal, err := c.DeepCopy(in.TypeMeta); err != nil {
2722 2755
 		return err
... ...
@@ -2778,6 +2811,16 @@ func deepCopy_api_RouteSpec(in routeapi.RouteSpec, out *routeapi.RouteSpec, c *c
2778 2778
 }
2779 2779
 
2780 2780
 func deepCopy_api_RouteStatus(in routeapi.RouteStatus, out *routeapi.RouteStatus, c *conversion.Cloner) error {
2781
+	if in.Ingress != nil {
2782
+		out.Ingress = make([]routeapi.RouteIngress, len(in.Ingress))
2783
+		for i := range in.Ingress {
2784
+			if err := deepCopy_api_RouteIngress(in.Ingress[i], &out.Ingress[i], c); err != nil {
2785
+				return err
2786
+			}
2787
+		}
2788
+	} else {
2789
+		out.Ingress = nil
2790
+	}
2781 2791
 	return nil
2782 2792
 }
2783 2793
 
... ...
@@ -3287,6 +3330,8 @@ func init() {
3287 3287
 		deepCopy_api_ProjectSpec,
3288 3288
 		deepCopy_api_ProjectStatus,
3289 3289
 		deepCopy_api_Route,
3290
+		deepCopy_api_RouteIngress,
3291
+		deepCopy_api_RouteIngressCondition,
3290 3292
 		deepCopy_api_RouteList,
3291 3293
 		deepCopy_api_RoutePort,
3292 3294
 		deepCopy_api_RouteSpec,
... ...
@@ -5026,6 +5026,53 @@ func Convert_api_Route_To_v1_Route(in *routeapi.Route, out *routeapiv1.Route, s
5026 5026
 	return autoConvert_api_Route_To_v1_Route(in, out, s)
5027 5027
 }
5028 5028
 
5029
+func autoConvert_api_RouteIngress_To_v1_RouteIngress(in *routeapi.RouteIngress, out *routeapiv1.RouteIngress, s conversion.Scope) error {
5030
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5031
+		defaulting.(func(*routeapi.RouteIngress))(in)
5032
+	}
5033
+	out.Host = in.Host
5034
+	out.RouterName = in.RouterName
5035
+	if in.Conditions != nil {
5036
+		out.Conditions = make([]routeapiv1.RouteIngressCondition, len(in.Conditions))
5037
+		for i := range in.Conditions {
5038
+			if err := Convert_api_RouteIngressCondition_To_v1_RouteIngressCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil {
5039
+				return err
5040
+			}
5041
+		}
5042
+	} else {
5043
+		out.Conditions = nil
5044
+	}
5045
+	return nil
5046
+}
5047
+
5048
+func Convert_api_RouteIngress_To_v1_RouteIngress(in *routeapi.RouteIngress, out *routeapiv1.RouteIngress, s conversion.Scope) error {
5049
+	return autoConvert_api_RouteIngress_To_v1_RouteIngress(in, out, s)
5050
+}
5051
+
5052
+func autoConvert_api_RouteIngressCondition_To_v1_RouteIngressCondition(in *routeapi.RouteIngressCondition, out *routeapiv1.RouteIngressCondition, s conversion.Scope) error {
5053
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5054
+		defaulting.(func(*routeapi.RouteIngressCondition))(in)
5055
+	}
5056
+	out.Type = routeapiv1.RouteIngressConditionType(in.Type)
5057
+	out.Status = apiv1.ConditionStatus(in.Status)
5058
+	out.Reason = in.Reason
5059
+	out.Message = in.Message
5060
+	// unable to generate simple pointer conversion for unversioned.Time -> unversioned.Time
5061
+	if in.LastTransitionTime != nil {
5062
+		out.LastTransitionTime = new(unversioned.Time)
5063
+		if err := api.Convert_unversioned_Time_To_unversioned_Time(in.LastTransitionTime, out.LastTransitionTime, s); err != nil {
5064
+			return err
5065
+		}
5066
+	} else {
5067
+		out.LastTransitionTime = nil
5068
+	}
5069
+	return nil
5070
+}
5071
+
5072
+func Convert_api_RouteIngressCondition_To_v1_RouteIngressCondition(in *routeapi.RouteIngressCondition, out *routeapiv1.RouteIngressCondition, s conversion.Scope) error {
5073
+	return autoConvert_api_RouteIngressCondition_To_v1_RouteIngressCondition(in, out, s)
5074
+}
5075
+
5029 5076
 func autoConvert_api_RouteList_To_v1_RouteList(in *routeapi.RouteList, out *routeapiv1.RouteList, s conversion.Scope) error {
5030 5077
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5031 5078
 		defaulting.(func(*routeapi.RouteList))(in)
... ...
@@ -5102,6 +5149,16 @@ func autoConvert_api_RouteStatus_To_v1_RouteStatus(in *routeapi.RouteStatus, out
5102 5102
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5103 5103
 		defaulting.(func(*routeapi.RouteStatus))(in)
5104 5104
 	}
5105
+	if in.Ingress != nil {
5106
+		out.Ingress = make([]routeapiv1.RouteIngress, len(in.Ingress))
5107
+		for i := range in.Ingress {
5108
+			if err := Convert_api_RouteIngress_To_v1_RouteIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
5109
+				return err
5110
+			}
5111
+		}
5112
+	} else {
5113
+		out.Ingress = nil
5114
+	}
5105 5115
 	return nil
5106 5116
 }
5107 5117
 
... ...
@@ -5146,6 +5203,53 @@ func Convert_v1_Route_To_api_Route(in *routeapiv1.Route, out *routeapi.Route, s
5146 5146
 	return autoConvert_v1_Route_To_api_Route(in, out, s)
5147 5147
 }
5148 5148
 
5149
+func autoConvert_v1_RouteIngress_To_api_RouteIngress(in *routeapiv1.RouteIngress, out *routeapi.RouteIngress, s conversion.Scope) error {
5150
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5151
+		defaulting.(func(*routeapiv1.RouteIngress))(in)
5152
+	}
5153
+	out.Host = in.Host
5154
+	out.RouterName = in.RouterName
5155
+	if in.Conditions != nil {
5156
+		out.Conditions = make([]routeapi.RouteIngressCondition, len(in.Conditions))
5157
+		for i := range in.Conditions {
5158
+			if err := Convert_v1_RouteIngressCondition_To_api_RouteIngressCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil {
5159
+				return err
5160
+			}
5161
+		}
5162
+	} else {
5163
+		out.Conditions = nil
5164
+	}
5165
+	return nil
5166
+}
5167
+
5168
+func Convert_v1_RouteIngress_To_api_RouteIngress(in *routeapiv1.RouteIngress, out *routeapi.RouteIngress, s conversion.Scope) error {
5169
+	return autoConvert_v1_RouteIngress_To_api_RouteIngress(in, out, s)
5170
+}
5171
+
5172
+func autoConvert_v1_RouteIngressCondition_To_api_RouteIngressCondition(in *routeapiv1.RouteIngressCondition, out *routeapi.RouteIngressCondition, s conversion.Scope) error {
5173
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5174
+		defaulting.(func(*routeapiv1.RouteIngressCondition))(in)
5175
+	}
5176
+	out.Type = routeapi.RouteIngressConditionType(in.Type)
5177
+	out.Status = api.ConditionStatus(in.Status)
5178
+	out.Reason = in.Reason
5179
+	out.Message = in.Message
5180
+	// unable to generate simple pointer conversion for unversioned.Time -> unversioned.Time
5181
+	if in.LastTransitionTime != nil {
5182
+		out.LastTransitionTime = new(unversioned.Time)
5183
+		if err := api.Convert_unversioned_Time_To_unversioned_Time(in.LastTransitionTime, out.LastTransitionTime, s); err != nil {
5184
+			return err
5185
+		}
5186
+	} else {
5187
+		out.LastTransitionTime = nil
5188
+	}
5189
+	return nil
5190
+}
5191
+
5192
+func Convert_v1_RouteIngressCondition_To_api_RouteIngressCondition(in *routeapiv1.RouteIngressCondition, out *routeapi.RouteIngressCondition, s conversion.Scope) error {
5193
+	return autoConvert_v1_RouteIngressCondition_To_api_RouteIngressCondition(in, out, s)
5194
+}
5195
+
5149 5196
 func autoConvert_v1_RouteList_To_api_RouteList(in *routeapiv1.RouteList, out *routeapi.RouteList, s conversion.Scope) error {
5150 5197
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5151 5198
 		defaulting.(func(*routeapiv1.RouteList))(in)
... ...
@@ -5222,6 +5326,16 @@ func autoConvert_v1_RouteStatus_To_api_RouteStatus(in *routeapiv1.RouteStatus, o
5222 5222
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
5223 5223
 		defaulting.(func(*routeapiv1.RouteStatus))(in)
5224 5224
 	}
5225
+	if in.Ingress != nil {
5226
+		out.Ingress = make([]routeapi.RouteIngress, len(in.Ingress))
5227
+		for i := range in.Ingress {
5228
+			if err := Convert_v1_RouteIngress_To_api_RouteIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
5229
+				return err
5230
+			}
5231
+		}
5232
+	} else {
5233
+		out.Ingress = nil
5234
+	}
5225 5235
 	return nil
5226 5236
 }
5227 5237
 
... ...
@@ -8444,6 +8558,8 @@ func init() {
8444 8444
 		autoConvert_api_RoleList_To_v1_RoleList,
8445 8445
 		autoConvert_api_Role_To_v1_Role,
8446 8446
 		autoConvert_api_RollingDeploymentStrategyParams_To_v1_RollingDeploymentStrategyParams,
8447
+		autoConvert_api_RouteIngressCondition_To_v1_RouteIngressCondition,
8448
+		autoConvert_api_RouteIngress_To_v1_RouteIngress,
8447 8449
 		autoConvert_api_RouteList_To_v1_RouteList,
8448 8450
 		autoConvert_api_RoutePort_To_v1_RoutePort,
8449 8451
 		autoConvert_api_RouteSpec_To_v1_RouteSpec,
... ...
@@ -8613,6 +8729,8 @@ func init() {
8613 8613
 		autoConvert_v1_RoleList_To_api_RoleList,
8614 8614
 		autoConvert_v1_Role_To_api_Role,
8615 8615
 		autoConvert_v1_RollingDeploymentStrategyParams_To_api_RollingDeploymentStrategyParams,
8616
+		autoConvert_v1_RouteIngressCondition_To_api_RouteIngressCondition,
8617
+		autoConvert_v1_RouteIngress_To_api_RouteIngress,
8616 8618
 		autoConvert_v1_RouteList_To_api_RouteList,
8617 8619
 		autoConvert_v1_RoutePort_To_api_RoutePort,
8618 8620
 		autoConvert_v1_RouteSpec_To_api_RouteSpec,
... ...
@@ -2604,6 +2604,39 @@ func deepCopy_v1_Route(in routeapiv1.Route, out *routeapiv1.Route, c *conversion
2604 2604
 	return nil
2605 2605
 }
2606 2606
 
2607
+func deepCopy_v1_RouteIngress(in routeapiv1.RouteIngress, out *routeapiv1.RouteIngress, c *conversion.Cloner) error {
2608
+	out.Host = in.Host
2609
+	out.RouterName = in.RouterName
2610
+	if in.Conditions != nil {
2611
+		out.Conditions = make([]routeapiv1.RouteIngressCondition, len(in.Conditions))
2612
+		for i := range in.Conditions {
2613
+			if err := deepCopy_v1_RouteIngressCondition(in.Conditions[i], &out.Conditions[i], c); err != nil {
2614
+				return err
2615
+			}
2616
+		}
2617
+	} else {
2618
+		out.Conditions = nil
2619
+	}
2620
+	return nil
2621
+}
2622
+
2623
+func deepCopy_v1_RouteIngressCondition(in routeapiv1.RouteIngressCondition, out *routeapiv1.RouteIngressCondition, c *conversion.Cloner) error {
2624
+	out.Type = in.Type
2625
+	out.Status = in.Status
2626
+	out.Reason = in.Reason
2627
+	out.Message = in.Message
2628
+	if in.LastTransitionTime != nil {
2629
+		if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil {
2630
+			return err
2631
+		} else {
2632
+			out.LastTransitionTime = newVal.(*unversioned.Time)
2633
+		}
2634
+	} else {
2635
+		out.LastTransitionTime = nil
2636
+	}
2637
+	return nil
2638
+}
2639
+
2607 2640
 func deepCopy_v1_RouteList(in routeapiv1.RouteList, out *routeapiv1.RouteList, c *conversion.Cloner) error {
2608 2641
 	if newVal, err := c.DeepCopy(in.TypeMeta); err != nil {
2609 2642
 		return err
... ...
@@ -2665,6 +2698,16 @@ func deepCopy_v1_RouteSpec(in routeapiv1.RouteSpec, out *routeapiv1.RouteSpec, c
2665 2665
 }
2666 2666
 
2667 2667
 func deepCopy_v1_RouteStatus(in routeapiv1.RouteStatus, out *routeapiv1.RouteStatus, c *conversion.Cloner) error {
2668
+	if in.Ingress != nil {
2669
+		out.Ingress = make([]routeapiv1.RouteIngress, len(in.Ingress))
2670
+		for i := range in.Ingress {
2671
+			if err := deepCopy_v1_RouteIngress(in.Ingress[i], &out.Ingress[i], c); err != nil {
2672
+				return err
2673
+			}
2674
+		}
2675
+	} else {
2676
+		out.Ingress = nil
2677
+	}
2668 2678
 	return nil
2669 2679
 }
2670 2680
 
... ...
@@ -3174,6 +3217,8 @@ func init() {
3174 3174
 		deepCopy_v1_ProjectSpec,
3175 3175
 		deepCopy_v1_ProjectStatus,
3176 3176
 		deepCopy_v1_Route,
3177
+		deepCopy_v1_RouteIngress,
3178
+		deepCopy_v1_RouteIngressCondition,
3177 3179
 		deepCopy_v1_RouteList,
3178 3180
 		deepCopy_v1_RoutePort,
3179 3181
 		deepCopy_v1_RouteSpec,
... ...
@@ -3891,6 +3891,53 @@ func Convert_api_Route_To_v1beta3_Route(in *routeapi.Route, out *routeapiv1beta3
3891 3891
 	return autoConvert_api_Route_To_v1beta3_Route(in, out, s)
3892 3892
 }
3893 3893
 
3894
+func autoConvert_api_RouteIngress_To_v1beta3_RouteIngress(in *routeapi.RouteIngress, out *routeapiv1beta3.RouteIngress, s conversion.Scope) error {
3895
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
3896
+		defaulting.(func(*routeapi.RouteIngress))(in)
3897
+	}
3898
+	out.Host = in.Host
3899
+	out.RouterName = in.RouterName
3900
+	if in.Conditions != nil {
3901
+		out.Conditions = make([]routeapiv1beta3.RouteIngressCondition, len(in.Conditions))
3902
+		for i := range in.Conditions {
3903
+			if err := Convert_api_RouteIngressCondition_To_v1beta3_RouteIngressCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil {
3904
+				return err
3905
+			}
3906
+		}
3907
+	} else {
3908
+		out.Conditions = nil
3909
+	}
3910
+	return nil
3911
+}
3912
+
3913
+func Convert_api_RouteIngress_To_v1beta3_RouteIngress(in *routeapi.RouteIngress, out *routeapiv1beta3.RouteIngress, s conversion.Scope) error {
3914
+	return autoConvert_api_RouteIngress_To_v1beta3_RouteIngress(in, out, s)
3915
+}
3916
+
3917
+func autoConvert_api_RouteIngressCondition_To_v1beta3_RouteIngressCondition(in *routeapi.RouteIngressCondition, out *routeapiv1beta3.RouteIngressCondition, s conversion.Scope) error {
3918
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
3919
+		defaulting.(func(*routeapi.RouteIngressCondition))(in)
3920
+	}
3921
+	out.Type = routeapiv1beta3.RouteIngressConditionType(in.Type)
3922
+	out.Status = apiv1beta3.ConditionStatus(in.Status)
3923
+	out.Reason = in.Reason
3924
+	out.Message = in.Message
3925
+	// unable to generate simple pointer conversion for unversioned.Time -> unversioned.Time
3926
+	if in.LastTransitionTime != nil {
3927
+		out.LastTransitionTime = new(unversioned.Time)
3928
+		if err := api.Convert_unversioned_Time_To_unversioned_Time(in.LastTransitionTime, out.LastTransitionTime, s); err != nil {
3929
+			return err
3930
+		}
3931
+	} else {
3932
+		out.LastTransitionTime = nil
3933
+	}
3934
+	return nil
3935
+}
3936
+
3937
+func Convert_api_RouteIngressCondition_To_v1beta3_RouteIngressCondition(in *routeapi.RouteIngressCondition, out *routeapiv1beta3.RouteIngressCondition, s conversion.Scope) error {
3938
+	return autoConvert_api_RouteIngressCondition_To_v1beta3_RouteIngressCondition(in, out, s)
3939
+}
3940
+
3894 3941
 func autoConvert_api_RouteList_To_v1beta3_RouteList(in *routeapi.RouteList, out *routeapiv1beta3.RouteList, s conversion.Scope) error {
3895 3942
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
3896 3943
 		defaulting.(func(*routeapi.RouteList))(in)
... ...
@@ -3967,6 +4014,16 @@ func autoConvert_api_RouteStatus_To_v1beta3_RouteStatus(in *routeapi.RouteStatus
3967 3967
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
3968 3968
 		defaulting.(func(*routeapi.RouteStatus))(in)
3969 3969
 	}
3970
+	if in.Ingress != nil {
3971
+		out.Ingress = make([]routeapiv1beta3.RouteIngress, len(in.Ingress))
3972
+		for i := range in.Ingress {
3973
+			if err := Convert_api_RouteIngress_To_v1beta3_RouteIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
3974
+				return err
3975
+			}
3976
+		}
3977
+	} else {
3978
+		out.Ingress = nil
3979
+	}
3970 3980
 	return nil
3971 3981
 }
3972 3982
 
... ...
@@ -4011,6 +4068,53 @@ func Convert_v1beta3_Route_To_api_Route(in *routeapiv1beta3.Route, out *routeapi
4011 4011
 	return autoConvert_v1beta3_Route_To_api_Route(in, out, s)
4012 4012
 }
4013 4013
 
4014
+func autoConvert_v1beta3_RouteIngress_To_api_RouteIngress(in *routeapiv1beta3.RouteIngress, out *routeapi.RouteIngress, s conversion.Scope) error {
4015
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
4016
+		defaulting.(func(*routeapiv1beta3.RouteIngress))(in)
4017
+	}
4018
+	out.Host = in.Host
4019
+	out.RouterName = in.RouterName
4020
+	if in.Conditions != nil {
4021
+		out.Conditions = make([]routeapi.RouteIngressCondition, len(in.Conditions))
4022
+		for i := range in.Conditions {
4023
+			if err := Convert_v1beta3_RouteIngressCondition_To_api_RouteIngressCondition(&in.Conditions[i], &out.Conditions[i], s); err != nil {
4024
+				return err
4025
+			}
4026
+		}
4027
+	} else {
4028
+		out.Conditions = nil
4029
+	}
4030
+	return nil
4031
+}
4032
+
4033
+func Convert_v1beta3_RouteIngress_To_api_RouteIngress(in *routeapiv1beta3.RouteIngress, out *routeapi.RouteIngress, s conversion.Scope) error {
4034
+	return autoConvert_v1beta3_RouteIngress_To_api_RouteIngress(in, out, s)
4035
+}
4036
+
4037
+func autoConvert_v1beta3_RouteIngressCondition_To_api_RouteIngressCondition(in *routeapiv1beta3.RouteIngressCondition, out *routeapi.RouteIngressCondition, s conversion.Scope) error {
4038
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
4039
+		defaulting.(func(*routeapiv1beta3.RouteIngressCondition))(in)
4040
+	}
4041
+	out.Type = routeapi.RouteIngressConditionType(in.Type)
4042
+	out.Status = api.ConditionStatus(in.Status)
4043
+	out.Reason = in.Reason
4044
+	out.Message = in.Message
4045
+	// unable to generate simple pointer conversion for unversioned.Time -> unversioned.Time
4046
+	if in.LastTransitionTime != nil {
4047
+		out.LastTransitionTime = new(unversioned.Time)
4048
+		if err := api.Convert_unversioned_Time_To_unversioned_Time(in.LastTransitionTime, out.LastTransitionTime, s); err != nil {
4049
+			return err
4050
+		}
4051
+	} else {
4052
+		out.LastTransitionTime = nil
4053
+	}
4054
+	return nil
4055
+}
4056
+
4057
+func Convert_v1beta3_RouteIngressCondition_To_api_RouteIngressCondition(in *routeapiv1beta3.RouteIngressCondition, out *routeapi.RouteIngressCondition, s conversion.Scope) error {
4058
+	return autoConvert_v1beta3_RouteIngressCondition_To_api_RouteIngressCondition(in, out, s)
4059
+}
4060
+
4014 4061
 func autoConvert_v1beta3_RouteList_To_api_RouteList(in *routeapiv1beta3.RouteList, out *routeapi.RouteList, s conversion.Scope) error {
4015 4062
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
4016 4063
 		defaulting.(func(*routeapiv1beta3.RouteList))(in)
... ...
@@ -4087,6 +4191,16 @@ func autoConvert_v1beta3_RouteStatus_To_api_RouteStatus(in *routeapiv1beta3.Rout
4087 4087
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
4088 4088
 		defaulting.(func(*routeapiv1beta3.RouteStatus))(in)
4089 4089
 	}
4090
+	if in.Ingress != nil {
4091
+		out.Ingress = make([]routeapi.RouteIngress, len(in.Ingress))
4092
+		for i := range in.Ingress {
4093
+			if err := Convert_v1beta3_RouteIngress_To_api_RouteIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
4094
+				return err
4095
+			}
4096
+		}
4097
+	} else {
4098
+		out.Ingress = nil
4099
+	}
4090 4100
 	return nil
4091 4101
 }
4092 4102
 
... ...
@@ -6747,6 +6861,8 @@ func init() {
6747 6747
 		autoConvert_api_RoleList_To_v1beta3_RoleList,
6748 6748
 		autoConvert_api_Role_To_v1beta3_Role,
6749 6749
 		autoConvert_api_RollingDeploymentStrategyParams_To_v1beta3_RollingDeploymentStrategyParams,
6750
+		autoConvert_api_RouteIngressCondition_To_v1beta3_RouteIngressCondition,
6751
+		autoConvert_api_RouteIngress_To_v1beta3_RouteIngress,
6750 6752
 		autoConvert_api_RouteList_To_v1beta3_RouteList,
6751 6753
 		autoConvert_api_RoutePort_To_v1beta3_RoutePort,
6752 6754
 		autoConvert_api_RouteSpec_To_v1beta3_RouteSpec,
... ...
@@ -6888,6 +7004,8 @@ func init() {
6888 6888
 		autoConvert_v1beta3_RoleList_To_api_RoleList,
6889 6889
 		autoConvert_v1beta3_Role_To_api_Role,
6890 6890
 		autoConvert_v1beta3_RollingDeploymentStrategyParams_To_api_RollingDeploymentStrategyParams,
6891
+		autoConvert_v1beta3_RouteIngressCondition_To_api_RouteIngressCondition,
6892
+		autoConvert_v1beta3_RouteIngress_To_api_RouteIngress,
6891 6893
 		autoConvert_v1beta3_RouteList_To_api_RouteList,
6892 6894
 		autoConvert_v1beta3_RoutePort_To_api_RoutePort,
6893 6895
 		autoConvert_v1beta3_RouteSpec_To_api_RouteSpec,
... ...
@@ -2441,6 +2441,39 @@ func deepCopy_v1beta3_Route(in routeapiv1beta3.Route, out *routeapiv1beta3.Route
2441 2441
 	return nil
2442 2442
 }
2443 2443
 
2444
+func deepCopy_v1beta3_RouteIngress(in routeapiv1beta3.RouteIngress, out *routeapiv1beta3.RouteIngress, c *conversion.Cloner) error {
2445
+	out.Host = in.Host
2446
+	out.RouterName = in.RouterName
2447
+	if in.Conditions != nil {
2448
+		out.Conditions = make([]routeapiv1beta3.RouteIngressCondition, len(in.Conditions))
2449
+		for i := range in.Conditions {
2450
+			if err := deepCopy_v1beta3_RouteIngressCondition(in.Conditions[i], &out.Conditions[i], c); err != nil {
2451
+				return err
2452
+			}
2453
+		}
2454
+	} else {
2455
+		out.Conditions = nil
2456
+	}
2457
+	return nil
2458
+}
2459
+
2460
+func deepCopy_v1beta3_RouteIngressCondition(in routeapiv1beta3.RouteIngressCondition, out *routeapiv1beta3.RouteIngressCondition, c *conversion.Cloner) error {
2461
+	out.Type = in.Type
2462
+	out.Status = in.Status
2463
+	out.Reason = in.Reason
2464
+	out.Message = in.Message
2465
+	if in.LastTransitionTime != nil {
2466
+		if newVal, err := c.DeepCopy(in.LastTransitionTime); err != nil {
2467
+			return err
2468
+		} else {
2469
+			out.LastTransitionTime = newVal.(*unversioned.Time)
2470
+		}
2471
+	} else {
2472
+		out.LastTransitionTime = nil
2473
+	}
2474
+	return nil
2475
+}
2476
+
2444 2477
 func deepCopy_v1beta3_RouteList(in routeapiv1beta3.RouteList, out *routeapiv1beta3.RouteList, c *conversion.Cloner) error {
2445 2478
 	if newVal, err := c.DeepCopy(in.TypeMeta); err != nil {
2446 2479
 		return err
... ...
@@ -2502,6 +2535,16 @@ func deepCopy_v1beta3_RouteSpec(in routeapiv1beta3.RouteSpec, out *routeapiv1bet
2502 2502
 }
2503 2503
 
2504 2504
 func deepCopy_v1beta3_RouteStatus(in routeapiv1beta3.RouteStatus, out *routeapiv1beta3.RouteStatus, c *conversion.Cloner) error {
2505
+	if in.Ingress != nil {
2506
+		out.Ingress = make([]routeapiv1beta3.RouteIngress, len(in.Ingress))
2507
+		for i := range in.Ingress {
2508
+			if err := deepCopy_v1beta3_RouteIngress(in.Ingress[i], &out.Ingress[i], c); err != nil {
2509
+				return err
2510
+			}
2511
+		}
2512
+	} else {
2513
+		out.Ingress = nil
2514
+	}
2505 2515
 	return nil
2506 2516
 }
2507 2517
 
... ...
@@ -3004,6 +3047,8 @@ func init() {
3004 3004
 		deepCopy_v1beta3_ProjectSpec,
3005 3005
 		deepCopy_v1beta3_ProjectStatus,
3006 3006
 		deepCopy_v1beta3_Route,
3007
+		deepCopy_v1beta3_RouteIngress,
3008
+		deepCopy_v1beta3_RouteIngressCondition,
3007 3009
 		deepCopy_v1beta3_RouteList,
3008 3010
 		deepCopy_v1beta3_RoutePort,
3009 3011
 		deepCopy_v1beta3_RouteSpec,
... ...
@@ -18,6 +18,7 @@ type RouteInterface interface {
18 18
 	Get(name string) (*routeapi.Route, error)
19 19
 	Create(route *routeapi.Route) (*routeapi.Route, error)
20 20
 	Update(route *routeapi.Route) (*routeapi.Route, error)
21
+	UpdateStatus(route *routeapi.Route) (*routeapi.Route, error)
21 22
 	Delete(name string) error
22 23
 	Watch(opts kapi.ListOptions) (watch.Interface, error)
23 24
 }
... ...
@@ -74,6 +75,13 @@ func (c *routes) Update(route *routeapi.Route) (result *routeapi.Route, err erro
74 74
 	return
75 75
 }
76 76
 
77
+// UpdateStatus takes the route with altered status.  Returns the server's representation of the route, and an error, if it occurs.
78
+func (c *routes) UpdateStatus(route *routeapi.Route) (result *routeapi.Route, err error) {
79
+	result = &routeapi.Route{}
80
+	err = c.r.Put().Namespace(c.ns).Resource("routes").Name(route.Name).SubResource("status").Body(route).Do().Into(result)
81
+	return
82
+}
83
+
77 84
 // Watch returns a watch.Interface that watches the requested routes.
78 85
 func (c *routes) Watch(opts kapi.ListOptions) (watch.Interface, error) {
79 86
 	return c.r.Get().
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"k8s.io/kubernetes/pkg/runtime"
10 10
 	"k8s.io/kubernetes/pkg/watch"
11 11
 
12
+	_ "github.com/openshift/origin/pkg/api/install"
12 13
 	"github.com/openshift/origin/pkg/client"
13 14
 )
14 15
 
... ...
@@ -51,6 +51,17 @@ func (c *FakeRoutes) Update(inObj *routeapi.Route) (*routeapi.Route, error) {
51 51
 	return obj.(*routeapi.Route), err
52 52
 }
53 53
 
54
+func (c *FakeRoutes) UpdateStatus(inObj *routeapi.Route) (*routeapi.Route, error) {
55
+	action := ktestclient.NewUpdateAction("routes", c.Namespace, inObj)
56
+	action.Subresource = "status"
57
+	obj, err := c.Fake.Invokes(action, inObj)
58
+	if obj == nil {
59
+		return nil, err
60
+	}
61
+
62
+	return obj.(*routeapi.Route), err
63
+}
64
+
54 65
 func (c *FakeRoutes) Delete(name string) error {
55 66
 	_, err := c.Fake.Invokes(ktestclient.NewDeleteAction("routes", c.Namespace, name), &routeapi.Route{})
56 67
 	return err
... ...
@@ -595,9 +595,56 @@ func (d *RouteDescriber) Describe(namespace, name string) (string, error) {
595 595
 
596 596
 	return tabbedString(func(out *tabwriter.Writer) error {
597 597
 		formatMeta(out, route.ObjectMeta)
598
-		formatString(out, "Host", route.Spec.Host)
598
+		if len(route.Spec.Host) > 0 {
599
+			formatString(out, "Requested Host", route.Spec.Host)
600
+			for _, ingress := range route.Status.Ingress {
601
+				if route.Spec.Host != ingress.Host {
602
+					continue
603
+				}
604
+				switch status, condition := ingressConditionStatus(&ingress, routeapi.RouteAdmitted); status {
605
+				case kapi.ConditionTrue:
606
+					fmt.Fprintf(out, "\t  exposed on router %s %s ago\n", ingress.RouterName, strings.ToLower(formatRelativeTime(condition.LastTransitionTime.Time)))
607
+				case kapi.ConditionFalse:
608
+					fmt.Fprintf(out, "\t  rejected by router %s: %s (%s ago)\n", ingress.RouterName, condition.Reason, strings.ToLower(formatRelativeTime(condition.LastTransitionTime.Time)))
609
+					if len(condition.Message) > 0 {
610
+						fmt.Fprintf(out, "\t    %s\n", condition.Message)
611
+					}
612
+				}
613
+			}
614
+		} else {
615
+			formatString(out, "Requested Host", "<auto>")
616
+		}
617
+		for _, ingress := range route.Status.Ingress {
618
+			if route.Spec.Host == ingress.Host {
619
+				continue
620
+			}
621
+			switch status, condition := ingressConditionStatus(&ingress, routeapi.RouteAdmitted); status {
622
+			case kapi.ConditionTrue:
623
+				fmt.Fprintf(out, "\t%s exposed on router %s %s ago\n", ingress.Host, ingress.RouterName, strings.ToLower(formatRelativeTime(condition.LastTransitionTime.Time)))
624
+			case kapi.ConditionFalse:
625
+				fmt.Fprintf(out, "\trejected by router %s: %s (%s ago)\n", ingress.RouterName, condition.Reason, strings.ToLower(formatRelativeTime(condition.LastTransitionTime.Time)))
626
+				if len(condition.Message) > 0 {
627
+					fmt.Fprintf(out, "\t  %s\n", condition.Message)
628
+				}
629
+			}
630
+		}
599 631
 		formatString(out, "Path", route.Spec.Path)
632
+
633
+		tlsTerm := ""
634
+		insecurePolicy := ""
635
+		if route.Spec.TLS != nil {
636
+			tlsTerm = string(route.Spec.TLS.Termination)
637
+			insecurePolicy = string(route.Spec.TLS.InsecureEdgeTerminationPolicy)
638
+		}
639
+		formatString(out, "TLS Termination", tlsTerm)
640
+		formatString(out, "Insecure Policy", insecurePolicy)
641
+
600 642
 		formatString(out, "Service", route.Spec.To.Name)
643
+		if route.Spec.Port != nil {
644
+			formatString(out, "Endpoint Port", route.Spec.Port.TargetPort.String())
645
+		} else {
646
+			formatString(out, "Endpoint Port", "<all endpoint ports>")
647
+		}
601 648
 
602 649
 		ends := "<none>"
603 650
 		if endsErr != nil {
... ...
@@ -619,21 +666,12 @@ func (d *RouteDescriber) Describe(namespace, name string) (string, error) {
619 619
 					}
620 620
 				}
621 621
 			}
622
-			ends = strings.Join(list, ",")
622
+			ends = strings.Join(list, ", ")
623 623
 			if count > max {
624 624
 				ends += fmt.Sprintf(" + %d more...", count-max)
625 625
 			}
626 626
 		}
627 627
 		formatString(out, "Endpoints", ends)
628
-
629
-		tlsTerm := ""
630
-		insecurePolicy := ""
631
-		if route.Spec.TLS != nil {
632
-			tlsTerm = string(route.Spec.TLS.Termination)
633
-			insecurePolicy = string(route.Spec.TLS.InsecureEdgeTerminationPolicy)
634
-		}
635
-		formatString(out, "TLS Termination", tlsTerm)
636
-		formatString(out, "Insecure Policy", insecurePolicy)
637 628
 		return nil
638 629
 	})
639 630
 }
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"text/tabwriter"
10 10
 	"time"
11 11
 
12
+	kapi "k8s.io/kubernetes/pkg/api"
12 13
 	"k8s.io/kubernetes/pkg/api/unversioned"
13 14
 	kctl "k8s.io/kubernetes/pkg/kubectl"
14 15
 	"k8s.io/kubernetes/pkg/labels"
... ...
@@ -34,7 +35,7 @@ var (
34 34
 	imageStreamImageColumns = []string{"NAME", "DOCKER REF", "UPDATED", "IMAGENAME"}
35 35
 	imageStreamColumns      = []string{"NAME", "DOCKER REPO", "TAGS", "UPDATED"}
36 36
 	projectColumns          = []string{"NAME", "DISPLAY NAME", "STATUS"}
37
-	routeColumns            = []string{"NAME", "HOST/PORT", "PATH", "SERVICE", "LABELS", "INSECURE POLICY", "TLS TERMINATION"}
37
+	routeColumns            = []string{"NAME", "HOST/PORT", "PATH", "SERVICE", "TERMINATION", "LABELS"}
38 38
 	deploymentColumns       = []string{"NAME", "STATUS", "CAUSE"}
39 39
 	deploymentConfigColumns = []string{"NAME", "REVISION", "REPLICAS", "TRIGGERED BY"}
40 40
 	templateColumns         = []string{"NAME", "DESCRIPTION", "PARAMETERS", "OBJECTS"}
... ...
@@ -425,6 +426,16 @@ func printProjectList(projects *projectapi.ProjectList, w io.Writer, opts kctl.P
425 425
 	return nil
426 426
 }
427 427
 
428
+func ingressConditionStatus(ingress *routeapi.RouteIngress, t routeapi.RouteIngressConditionType) (kapi.ConditionStatus, routeapi.RouteIngressCondition) {
429
+	for _, condition := range ingress.Conditions {
430
+		if t != condition.Type {
431
+			continue
432
+		}
433
+		return condition.Status, condition
434
+	}
435
+	return kapi.ConditionUnknown, routeapi.RouteIngressCondition{}
436
+}
437
+
428 438
 func printRoute(route *routeapi.Route, w io.Writer, opts kctl.PrintOptions) error {
429 439
 	tlsTerm := ""
430 440
 	insecurePolicy := ""
... ...
@@ -437,8 +448,57 @@ func printRoute(route *routeapi.Route, w io.Writer, opts kctl.PrintOptions) erro
437 437
 			return err
438 438
 		}
439 439
 	}
440
-	_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
441
-		route.Name, route.Spec.Host, route.Spec.Path, route.Spec.To.Name, labels.Set(route.Labels), insecurePolicy, tlsTerm)
440
+	var (
441
+		matchedHost bool
442
+		reason      string
443
+		host        = route.Spec.Host
444
+
445
+		admitted, errors = 0, 0
446
+	)
447
+	for _, ingress := range route.Status.Ingress {
448
+		switch status, condition := ingressConditionStatus(&ingress, routeapi.RouteAdmitted); status {
449
+		case kapi.ConditionTrue:
450
+			admitted++
451
+			if !matchedHost {
452
+				matchedHost = ingress.Host == route.Spec.Host
453
+				host = ingress.Host
454
+			}
455
+		case kapi.ConditionFalse:
456
+			reason = condition.Reason
457
+			errors++
458
+		}
459
+	}
460
+	switch {
461
+	case route.Status.Ingress == nil:
462
+		// this is the legacy case, we should continue to show the host when talking to servers
463
+		// that have not set status ingress, since we can't distinguish this condition from there
464
+		// being no routers.
465
+	case admitted == 0 && errors > 0:
466
+		host = reason
467
+	case errors > 0:
468
+		host = fmt.Sprintf("%s ... %d rejected", host, errors)
469
+	case admitted == 0:
470
+		host = "Pending"
471
+	case admitted > 1:
472
+		host = fmt.Sprintf("%s ... %d more", host, admitted-1)
473
+	}
474
+	var policy string
475
+	switch {
476
+	case len(tlsTerm) != 0 && len(insecurePolicy) != 0:
477
+		policy = fmt.Sprintf("%s/%s", tlsTerm, insecurePolicy)
478
+	case len(tlsTerm) != 0:
479
+		policy = tlsTerm
480
+	case len(insecurePolicy) != 0:
481
+		policy = fmt.Sprintf("default/%s", insecurePolicy)
482
+	default:
483
+		policy = ""
484
+	}
485
+	svc := route.Spec.To.Name
486
+	if route.Spec.Port != nil {
487
+		svc = fmt.Sprintf("%s:%s", svc, route.Spec.Port.TargetPort.String())
488
+	}
489
+	_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
490
+		route.Name, host, route.Spec.Path, svc, policy, labels.Set(route.Labels))
442 491
 	return err
443 492
 }
444 493
 
... ...
@@ -40,6 +40,8 @@ type F5RouterOptions struct {
40 40
 
41 41
 // F5Router is the config necessary to start an F5 router plugin.
42 42
 type F5Router struct {
43
+	RouterName string
44
+
43 45
 	// Host specifies the hostname or IP address of the F5 BIG-IP host.
44 46
 	Host string
45 47
 
... ...
@@ -76,6 +78,7 @@ type F5Router struct {
76 76
 
77 77
 // Bind binds F5Router arguments to flags
78 78
 func (o *F5Router) Bind(flag *pflag.FlagSet) {
79
+	flag.StringVar(&o.RouterName, "name", util.Env("ROUTER_SERVICE_NAME", "public"), "The name the router will identify itself with in the route status")
79 80
 	flag.StringVar(&o.Host, "f5-host", util.Env("ROUTER_EXTERNAL_HOST_HOSTNAME", ""), "The host of F5 BIG-IP's management interface")
80 81
 	flag.StringVar(&o.Username, "f5-username", util.Env("ROUTER_EXTERNAL_HOST_USERNAME", ""), "The username for F5 BIG-IP's management utility")
81 82
 	flag.StringVar(&o.Password, "f5-password", util.Env("ROUTER_EXTERNAL_HOST_PASSWORD", ""), "The password for F5 BIG-IP's management utility")
... ...
@@ -167,13 +170,14 @@ func (o *F5RouterOptions) Run() error {
167 167
 		return err
168 168
 	}
169 169
 
170
-	plugin := controller.NewUniqueHost(f5Plugin, o.RouteSelectionFunc())
171
-
172 170
 	oc, kc, err := o.Config.Clients()
173 171
 	if err != nil {
174 172
 		return err
175 173
 	}
176 174
 
175
+	statusPlugin := controller.NewStatusAdmitter(f5Plugin, oc, o.RouterName)
176
+	plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
177
+
177 178
 	factory := o.RouterSelection.NewFactory(oc, kc)
178 179
 	controller := factory.Create(plugin)
179 180
 	controller.Run()
... ...
@@ -45,6 +45,7 @@ type TemplateRouterOptions struct {
45 45
 }
46 46
 
47 47
 type TemplateRouter struct {
48
+	RouterName         string
48 49
 	WorkingDir         string
49 50
 	TemplateFile       string
50 51
 	ReloadScript       string
... ...
@@ -54,6 +55,7 @@ type TemplateRouter struct {
54 54
 }
55 55
 
56 56
 func (o *TemplateRouter) Bind(flag *pflag.FlagSet) {
57
+	flag.StringVar(&o.RouterName, "name", util.Env("ROUTER_SERVICE_NAME", "public"), "The name the router will identify itself with in the route status")
57 58
 	flag.StringVar(&o.WorkingDir, "working-dir", "/var/lib/containers/router", "The working directory for the router plugin")
58 59
 	flag.StringVar(&o.DefaultCertificate, "default-certificate", util.Env("DEFAULT_CERTIFICATE", ""), "A path to default certificate to use for routes that don't expose a TLS server cert; in PEM format")
59 60
 	flag.StringVar(&o.TemplateFile, "template", util.Env("TEMPLATE_FILE", ""), "The path to the template file to use")
... ...
@@ -139,6 +141,9 @@ func (o *TemplateRouterOptions) Complete() error {
139 139
 }
140 140
 
141 141
 func (o *TemplateRouterOptions) Validate() error {
142
+	if len(o.RouterName) == 0 {
143
+		return errors.New("router must have a name to identify itself in route status")
144
+	}
142 145
 	if len(o.TemplateFile) == 0 {
143 146
 		return errors.New("template file must be specified")
144 147
 	}
... ...
@@ -169,13 +174,14 @@ func (o *TemplateRouterOptions) Run() error {
169 169
 		return err
170 170
 	}
171 171
 
172
-	plugin := controller.NewUniqueHost(templatePlugin, o.RouteSelectionFunc())
173
-
174 172
 	oc, kc, err := o.Config.Clients()
175 173
 	if err != nil {
176 174
 		return err
177 175
 	}
178 176
 
177
+	statusPlugin := controller.NewStatusAdmitter(templatePlugin, oc, o.RouterName)
178
+	plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin)
179
+
179 180
 	factory := o.RouterSelection.NewFactory(oc, kc)
180 181
 	controller := factory.Create(plugin)
181 182
 	controller.Run()
... ...
@@ -117,6 +117,11 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
117 117
 					// this is used by verifyImageStreamAccess in pkg/dockerregistry/server/auth.go
118 118
 					Resources: sets.NewString("imagestreams/layers"),
119 119
 				},
120
+				// an admin can run routers that write back conditions to the route
121
+				{
122
+					Verbs:     sets.NewString("update"),
123
+					Resources: sets.NewString("routes/status"),
124
+				},
120 125
 			},
121 126
 		},
122 127
 		{
... ...
@@ -323,6 +328,11 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
323 323
 					Verbs:     sets.NewString("list", "watch"),
324 324
 					Resources: sets.NewString("routes", "endpoints"),
325 325
 				},
326
+				// routers write back conditions to the route
327
+				{
328
+					Verbs:     sets.NewString("update"),
329
+					Resources: sets.NewString("routes/status"),
330
+				},
326 331
 			},
327 332
 		},
328 333
 		{
... ...
@@ -49,6 +49,49 @@ type RoutePort struct {
49 49
 // RouteStatus provides relevant info about the status of a route, including which routers
50 50
 // acknowledge it.
51 51
 type RouteStatus struct {
52
+	// Ingress describes the places where the route may be exposed. The list of
53
+	// ingress points may contain duplicate Host or RouterName values. Routes
54
+	// are considered live once they are `Ready`
55
+	Ingress []RouteIngress
56
+}
57
+
58
+// RouteIngress holds information about the places where a route is exposed
59
+type RouteIngress struct {
60
+	// Host is the host string under which the route is exposed; this value is required
61
+	Host string
62
+	// Name is a name chosen by the router to identify itself; this value is required
63
+	RouterName string
64
+	// Conditions is the state of the route, may be empty.
65
+	Conditions []RouteIngressCondition
66
+}
67
+
68
+// RouteIngressConditionType is a valid value for RouteCondition
69
+type RouteIngressConditionType string
70
+
71
+// These are valid conditions of pod.
72
+const (
73
+	// RouteAdmitted means the route is able to service requests for the provided Host
74
+	RouteAdmitted RouteIngressConditionType = "Admitted"
75
+	// TODO: add other route condition types
76
+)
77
+
78
+// RouteIngressCondition contains details for the current condition of this pod.
79
+// TODO: add LastTransitionTime, Reason, Message to match NodeCondition api.
80
+type RouteIngressCondition struct {
81
+	// Type is the type of the condition.
82
+	// Currently only Ready.
83
+	Type RouteIngressConditionType
84
+	// Status is the status of the condition.
85
+	// Can be True, False, Unknown.
86
+	Status kapi.ConditionStatus
87
+	// (brief) reason for the condition's last transition, and is usually a machine and human
88
+	// readable constant
89
+	Reason string
90
+	// Human readable message indicating details about last transition.
91
+	Message string
92
+	// RFC 3339 date and time at which the object was acknowledged by the router.
93
+	// This may be before the router exposes the route
94
+	LastTransitionTime *unversioned.Time
52 95
 }
53 96
 
54 97
 // RouteList is a collection of Routes.
... ...
@@ -61,6 +61,48 @@ type RoutePort struct {
61 61
 // RouteStatus provides relevant info about the status of a route, including which routers
62 62
 // acknowledge it.
63 63
 type RouteStatus struct {
64
+	// Ingress describes the places where the route may be exposed. The list of
65
+	// ingress points may contain duplicate Host or RouterName values. Routes
66
+	// are considered live once they are `Ready`
67
+	Ingress []RouteIngress `json:"ingress" description:"traffic reaches this route via these ingress paths"`
68
+}
69
+
70
+// RouteIngress holds information about the places where a route is exposed
71
+type RouteIngress struct {
72
+	// Host is the host string under which the route is exposed; this value is required
73
+	Host string `json:"host,omitempty" description:"the host name this route is exposed to by the specified router"`
74
+	// Name is a name chosen by the router to identify itself; this value is required
75
+	RouterName string `json:"routerName,omitempty" description:"the name of the router exposing this route"`
76
+	// Conditions is the state of the route, may be empty.
77
+	Conditions []RouteIngressCondition `json:"conditions,omitempty" description:"the conditions that apply to this router" patchStrategy:"merge" patchMergeKey:"type"`
78
+}
79
+
80
+// RouteIngressConditionType is a valid value for RouteCondition
81
+type RouteIngressConditionType string
82
+
83
+// These are valid conditions of pod.
84
+const (
85
+	// RouteAdmitted means the route is able to service requests for the provided Host
86
+	RouteAdmitted RouteIngressConditionType = "Admitted"
87
+	// TODO: add other route condition types
88
+)
89
+
90
+// RouteIngressCondition contains details for the current condition of this pod.
91
+// TODO: add LastTransitionTime, Reason, Message to match NodeCondition api.
92
+type RouteIngressCondition struct {
93
+	// Type is the type of the condition.
94
+	// Currently only Ready.
95
+	Type RouteIngressConditionType `json:"type" description:"the type of the condition"`
96
+	// Status is the status of the condition.
97
+	// Can be True, False, Unknown.
98
+	Status kapi.ConditionStatus `json:"status" description:"status is the status of the condition; True, False, or Unknown"`
99
+	// (brief) reason for the condition's last transition, and is usually a machine and human
100
+	// readable constant
101
+	Reason string `json:"reason,omitempty" description:"brief reason for the condition's last transition, machine readable constant"`
102
+	// Human readable message indicating details about last transition.
103
+	Message string `json:"message,omitempty" description:"human readable message indicating details about this condition"`
104
+	// RFC 3339 date and time when this condition last transitioned
105
+	LastTransitionTime *unversioned.Time `json:"lastTransitionTime,omitempty" description:"the last time at which this condition transitioned to the current status"`
64 106
 }
65 107
 
66 108
 // RouterShard has information of a routing shard and is used to
... ...
@@ -57,6 +57,49 @@ type RoutePort struct {
57 57
 // RouteStatus provides relevant info about the status of a route, including which routers
58 58
 // acknowledge it.
59 59
 type RouteStatus struct {
60
+	// Ingress describes the places where the route may be exposed. The list of
61
+	// ingress points may contain duplicate Host or RouterName values. Routes
62
+	// are considered live once they are `Ready`
63
+	Ingress []RouteIngress `json:"ingress"`
64
+}
65
+
66
+// RouteIngress holds information about the places where a route is exposed
67
+type RouteIngress struct {
68
+	// Host is the host string under which the route is exposed; this value is required
69
+	Host string `json:"host,omitempty"`
70
+	// Name is a name chosen by the router to identify itself; this value is required
71
+	RouterName string `json:"routerName,omitempty"`
72
+	// Conditions is the state of the route, may be empty.
73
+	Conditions []RouteIngressCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
74
+}
75
+
76
+// RouteIngressConditionType is a valid value for RouteCondition
77
+type RouteIngressConditionType string
78
+
79
+// These are valid conditions of pod.
80
+const (
81
+	// RouteAdmitted means the route is able to service requests for the provided Host
82
+	RouteAdmitted RouteIngressConditionType = "Admitted"
83
+	// TODO: add other route condition types
84
+)
85
+
86
+// RouteIngressCondition contains details for the current condition of this pod.
87
+// TODO: add LastTransitionTime, Reason, Message to match NodeCondition api.
88
+type RouteIngressCondition struct {
89
+	// Type is the type of the condition.
90
+	// Currently only Ready.
91
+	Type RouteIngressConditionType `json:"type"`
92
+	// Status is the status of the condition.
93
+	// Can be True, False, Unknown.
94
+	Status kapi.ConditionStatus `json:"status"`
95
+	// (brief) reason for the condition's last transition, and is usually a machine and human
96
+	// readable constant
97
+	Reason string `json:"reason,omitempty"`
98
+	// Human readable message indicating details about last transition.
99
+	Message string `json:"message,omitempty"`
100
+	// RFC 3339 date and time at which the object was acknowledged by the router.
101
+	// This may be before the router exposes the route
102
+	LastTransitionTime *unversioned.Time `json:"lastTransitionTime,omitempty"`
60 103
 }
61 104
 
62 105
 // RouterShard has information of a routing shard and is used to
... ...
@@ -46,7 +46,10 @@ func NewREST(s storage.Interface, allocator route.RouteAllocator) (*REST, *Statu
46 46
 		Storage: s,
47 47
 	}
48 48
 
49
-	return &REST{store}, &StatusREST{store}
49
+	statusStore := *store
50
+	statusStore.UpdateStrategy = rest.StatusStrategy
51
+
52
+	return &REST{store}, &StatusREST{&statusStore}
50 53
 }
51 54
 
52 55
 // StatusREST implements the REST endpoint for changing the status of a route.
53 56
new file mode 100644
... ...
@@ -0,0 +1,238 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"time"
5
+
6
+	"github.com/golang/glog"
7
+	lru "github.com/hashicorp/golang-lru"
8
+
9
+	kapi "k8s.io/kubernetes/pkg/api"
10
+	"k8s.io/kubernetes/pkg/api/errors"
11
+	"k8s.io/kubernetes/pkg/api/unversioned"
12
+	"k8s.io/kubernetes/pkg/util"
13
+	"k8s.io/kubernetes/pkg/util/sets"
14
+	"k8s.io/kubernetes/pkg/watch"
15
+
16
+	"github.com/openshift/origin/pkg/client"
17
+	routeapi "github.com/openshift/origin/pkg/route/api"
18
+	"github.com/openshift/origin/pkg/router"
19
+)
20
+
21
+// StatusAdmitter ensures routes added to the plugin have status set.
22
+type StatusAdmitter struct {
23
+	plugin     router.Plugin
24
+	client     client.RoutesNamespacer
25
+	routerName string
26
+
27
+	contentionInterval time.Duration
28
+	expected           *lru.Cache
29
+}
30
+
31
+// NewStatusAdmitter creates a plugin wrapper that ensures every accepted
32
+// route has a status field set that matches this router. The admitter manages
33
+// an LRU of recently seen conflicting updates to handle when two router processes
34
+// with differing configurations are writing updates at the same time.
35
+func NewStatusAdmitter(plugin router.Plugin, client client.RoutesNamespacer, name string) *StatusAdmitter {
36
+	expected, _ := lru.New(1024)
37
+	return &StatusAdmitter{
38
+		plugin:     plugin,
39
+		client:     client,
40
+		routerName: name,
41
+
42
+		contentionInterval: 1 * time.Minute,
43
+		expected:           expected,
44
+	}
45
+}
46
+
47
+// nowFn allows the package to be tested
48
+var nowFn = unversioned.Now
49
+
50
+// findOrCreateIngress loops through the router status ingress array looking for an entry
51
+// that matches name. If there is no entry in the array, it creates one and appends it
52
+// to the array. If there are multiple entries with that name, the first one is
53
+// returned and later ones are removed. Changed is returned as true if any part of the
54
+// array is altered.
55
+func findOrCreateIngress(route *routeapi.Route, name string) (_ *routeapi.RouteIngress, changed bool) {
56
+	position := -1
57
+	updated := make([]routeapi.RouteIngress, 0, len(route.Status.Ingress))
58
+	for i := range route.Status.Ingress {
59
+		existing := &route.Status.Ingress[i]
60
+		if existing.RouterName != name {
61
+			updated = append(updated, *existing)
62
+			continue
63
+		}
64
+		if position != -1 {
65
+			changed = true
66
+			continue
67
+		}
68
+		updated = append(updated, *existing)
69
+		position = i
70
+	}
71
+	switch {
72
+	case position == -1:
73
+		position = len(route.Status.Ingress)
74
+		route.Status.Ingress = append(route.Status.Ingress, routeapi.RouteIngress{
75
+			RouterName: name,
76
+			Host:       route.Spec.Host,
77
+		})
78
+		changed = true
79
+	case changed:
80
+		route.Status.Ingress = updated
81
+	}
82
+	ingress := &route.Status.Ingress[position]
83
+	if ingress.Host != route.Spec.Host {
84
+		ingress.Host = route.Spec.Host
85
+		changed = true
86
+	}
87
+	return ingress, changed
88
+}
89
+
90
+// setIngressCondition records the condition on the ingress, returning true if the ingress was changed and
91
+// false if no modification was made.
92
+func setIngressCondition(ingress *routeapi.RouteIngress, condition routeapi.RouteIngressCondition) bool {
93
+	for _, existing := range ingress.Conditions {
94
+		//existing.LastTransitionTime = nil
95
+		if existing == condition {
96
+			return false
97
+		}
98
+	}
99
+	now := nowFn()
100
+	condition.LastTransitionTime = &now
101
+	ingress.Conditions = []routeapi.RouteIngressCondition{condition}
102
+	return true
103
+}
104
+
105
+func ingressConditionTouched(ingress *routeapi.RouteIngress) *unversioned.Time {
106
+	var lastTouch *unversioned.Time
107
+	for _, condition := range ingress.Conditions {
108
+		if t := condition.LastTransitionTime; t != nil {
109
+			switch {
110
+			case lastTouch == nil, t.After(lastTouch.Time):
111
+				lastTouch = t
112
+			}
113
+		}
114
+	}
115
+	return lastTouch
116
+}
117
+
118
+// recordIngressConditionFailure updates the matching ingress on the route (or adds a new one) with the specified
119
+// condition, returning true if the object was modified.
120
+func recordIngressConditionFailure(route *routeapi.Route, name string, condition routeapi.RouteIngressCondition) (*routeapi.RouteIngress, bool, *unversioned.Time) {
121
+	for i := range route.Status.Ingress {
122
+		existing := &route.Status.Ingress[i]
123
+		if existing.RouterName != name {
124
+			continue
125
+		}
126
+		lastTouch := ingressConditionTouched(existing)
127
+		return existing, setIngressCondition(existing, condition), lastTouch
128
+	}
129
+	route.Status.Ingress = append(route.Status.Ingress, routeapi.RouteIngress{RouterName: name})
130
+	ingress := &route.Status.Ingress[len(route.Status.Ingress)-1]
131
+	setIngressCondition(ingress, condition)
132
+	return ingress, true, nil
133
+}
134
+
135
+// hasIngressBeenTouched returns true if the route appears to have been touched since the last time
136
+func (a *StatusAdmitter) hasIngressBeenTouched(route *routeapi.Route, lastTouch *unversioned.Time) bool {
137
+	glog.V(4).Infof("has last touch %v for %s/%s", lastTouch, route.Namespace, route.Name)
138
+	if lastTouch.IsZero() {
139
+		return false
140
+	}
141
+	old, ok := a.expected.Get(route.UID)
142
+	if !ok || old.(time.Time).Equal(lastTouch.Time) {
143
+		return false
144
+	}
145
+	return true
146
+}
147
+
148
+// recordIngressTouch
149
+func (a *StatusAdmitter) recordIngressTouch(route *routeapi.Route, touch *unversioned.Time, err error) {
150
+	switch {
151
+	case err == nil:
152
+		if touch != nil {
153
+			a.expected.Add(route.UID, touch.Time)
154
+		}
155
+	case errors.IsConflict(err):
156
+		a.expected.Add(route.UID, time.Time{})
157
+	}
158
+}
159
+
160
+// admitRoute returns true if the route has already been accepted to this router, or
161
+// updates the route to contain an accepted condition. Returns an error if the route could
162
+// not be admitted.
163
+func (a *StatusAdmitter) admitRoute(oc client.RoutesNamespacer, route *routeapi.Route, name string) (bool, error) {
164
+	ingress, updated := findOrCreateIngress(route, name)
165
+	if !updated {
166
+		for i := range ingress.Conditions {
167
+			cond := &ingress.Conditions[i]
168
+			if cond.Type == routeapi.RouteAdmitted && cond.Status == kapi.ConditionTrue {
169
+				glog.V(4).Infof("admit: route already admitted")
170
+				return true, nil
171
+			}
172
+		}
173
+	}
174
+
175
+	if a.hasIngressBeenTouched(route, ingressConditionTouched(ingress)) {
176
+		glog.V(4).Infof("admit: observed a route update from someone else: route %s/%s has been updated to an inconsistent value, doing nothing", route.Namespace, route.Name)
177
+		return true, nil
178
+	}
179
+
180
+	setIngressCondition(ingress, routeapi.RouteIngressCondition{
181
+		Type:   routeapi.RouteAdmitted,
182
+		Status: kapi.ConditionTrue,
183
+	})
184
+	glog.V(4).Infof("admit: admitting route by updating status: %s (%t): %s", route.Name, updated, route.Spec.Host)
185
+	_, err := oc.Routes(route.Namespace).UpdateStatus(route)
186
+	a.recordIngressTouch(route, ingress.Conditions[0].LastTransitionTime, err)
187
+	return err == nil, err
188
+}
189
+
190
+// RecordRouteRejection attempts to update the route status with a reason for a route being rejected.
191
+func (a *StatusAdmitter) RecordRouteRejection(route *routeapi.Route, reason, message string) {
192
+	ingress, changed, lastTouch := recordIngressConditionFailure(route, a.routerName, routeapi.RouteIngressCondition{
193
+		Type:    routeapi.RouteAdmitted,
194
+		Status:  kapi.ConditionFalse,
195
+		Reason:  reason,
196
+		Message: message,
197
+	})
198
+	if !changed {
199
+		glog.V(4).Infof("reject: no changes to route needed: %s/%s", route.Namespace, route.Name)
200
+		return
201
+	}
202
+
203
+	if a.hasIngressBeenTouched(route, lastTouch) {
204
+		glog.V(4).Infof("reject: observed a route update from someone else: route %s/%s has been updated to an inconsistent value, doing nothing", route.Namespace, route.Name)
205
+		return
206
+	}
207
+
208
+	_, err := a.client.Routes(route.Namespace).UpdateStatus(route)
209
+	a.recordIngressTouch(route, ingress.Conditions[0].LastTransitionTime, err)
210
+	if err != nil {
211
+		util.HandleError(fmt.Errorf("unable to write route rejection to the status: %v", err))
212
+	}
213
+}
214
+
215
+// HandleRoute attempts to admit the provided route on watch add / modifications.
216
+func (a *StatusAdmitter) HandleRoute(eventType watch.EventType, route *routeapi.Route) error {
217
+	switch eventType {
218
+	case watch.Added, watch.Modified:
219
+		ok, err := a.admitRoute(a.client, route, a.routerName)
220
+		if err != nil {
221
+			return err
222
+		}
223
+		if !ok {
224
+			glog.V(4).Infof("skipping route: %s", route.Name)
225
+			return nil
226
+		}
227
+	}
228
+	return a.plugin.HandleRoute(eventType, route)
229
+}
230
+
231
+func (a *StatusAdmitter) HandleEndpoints(eventType watch.EventType, route *kapi.Endpoints) error {
232
+	return a.plugin.HandleEndpoints(eventType, route)
233
+}
234
+
235
+func (a *StatusAdmitter) HandleNamespaces(namespaces sets.String) error {
236
+	return a.plugin.HandleNamespaces(namespaces)
237
+}
0 238
new file mode 100644
... ...
@@ -0,0 +1,313 @@
0
+package controller
1
+
2
+import (
3
+	"fmt"
4
+	"reflect"
5
+	"testing"
6
+	"time"
7
+
8
+	kapi "k8s.io/kubernetes/pkg/api"
9
+	"k8s.io/kubernetes/pkg/api/errors"
10
+	"k8s.io/kubernetes/pkg/api/unversioned"
11
+	ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient"
12
+	"k8s.io/kubernetes/pkg/types"
13
+	"k8s.io/kubernetes/pkg/util/sets"
14
+	"k8s.io/kubernetes/pkg/watch"
15
+
16
+	"github.com/openshift/origin/pkg/client/testclient"
17
+	routeapi "github.com/openshift/origin/pkg/route/api"
18
+)
19
+
20
+type fakePlugin struct {
21
+	t     watch.EventType
22
+	route *routeapi.Route
23
+	err   error
24
+}
25
+
26
+func (p *fakePlugin) HandleRoute(t watch.EventType, route *routeapi.Route) error {
27
+	p.t, p.route = t, route
28
+	return p.err
29
+}
30
+func (p *fakePlugin) HandleEndpoints(watch.EventType, *kapi.Endpoints) error {
31
+	return fmt.Errorf("not expected")
32
+}
33
+func (p *fakePlugin) HandleNamespaces(namespaces sets.String) error {
34
+	return fmt.Errorf("not expected")
35
+}
36
+
37
+func TestStatusNoOp(t *testing.T) {
38
+	now := unversioned.Now()
39
+	touched := unversioned.Time{Time: now.Add(-time.Minute)}
40
+	p := &fakePlugin{}
41
+	c := testclient.NewSimpleFake()
42
+	admitter := NewStatusAdmitter(p, c, "test")
43
+	err := admitter.HandleRoute(watch.Added, &routeapi.Route{
44
+		ObjectMeta: kapi.ObjectMeta{Name: "route1", Namespace: "default", UID: types.UID("uid1")},
45
+		Spec:       routeapi.RouteSpec{Host: "route1.test.local"},
46
+		Status: routeapi.RouteStatus{
47
+			Ingress: []routeapi.RouteIngress{
48
+				{
49
+					Host:       "route1.test.local",
50
+					RouterName: "test",
51
+					Conditions: []routeapi.RouteIngressCondition{
52
+						{
53
+							Type:               routeapi.RouteAdmitted,
54
+							Status:             kapi.ConditionTrue,
55
+							LastTransitionTime: &touched,
56
+						},
57
+					},
58
+				},
59
+			},
60
+		},
61
+	})
62
+	if err != nil {
63
+		t.Fatalf("unexpected error: %v", err)
64
+	}
65
+	if len(c.Actions()) > 0 {
66
+		t.Fatalf("unexpected actions: %#v", c.Actions())
67
+	}
68
+}
69
+
70
+func TestStatusResetsHost(t *testing.T) {
71
+	now := unversioned.Now()
72
+	nowFn = func() unversioned.Time { return now }
73
+	touched := unversioned.Time{Time: now.Add(-time.Minute)}
74
+	p := &fakePlugin{}
75
+	c := testclient.NewSimpleFake(&routeapi.Route{})
76
+	admitter := NewStatusAdmitter(p, c, "test")
77
+	err := admitter.HandleRoute(watch.Added, &routeapi.Route{
78
+		ObjectMeta: kapi.ObjectMeta{Name: "route1", Namespace: "default", UID: types.UID("uid1")},
79
+		Spec:       routeapi.RouteSpec{Host: "route1.test.local"},
80
+		Status: routeapi.RouteStatus{
81
+			Ingress: []routeapi.RouteIngress{
82
+				{
83
+					Host:       "route2.test.local",
84
+					RouterName: "test",
85
+					Conditions: []routeapi.RouteIngressCondition{
86
+						{
87
+							Type:               routeapi.RouteAdmitted,
88
+							Status:             kapi.ConditionTrue,
89
+							LastTransitionTime: &touched,
90
+						},
91
+					},
92
+				},
93
+			},
94
+		},
95
+	})
96
+	if err != nil {
97
+		t.Fatalf("unexpected error: %v", err)
98
+	}
99
+	if len(c.Actions()) != 1 {
100
+		t.Fatalf("unexpected actions: %#v", c.Actions())
101
+	}
102
+	action := c.Actions()[0]
103
+	if action.GetVerb() != "update" || action.GetResource() != "routes" || action.GetSubresource() != "status" {
104
+		t.Fatalf("unexpected action: %#v", action)
105
+	}
106
+	obj := c.Actions()[0].(ktestclient.UpdateAction).GetObject().(*routeapi.Route)
107
+	if len(obj.Status.Ingress) != 1 && obj.Status.Ingress[0].Host != "route1.test.local" {
108
+		t.Fatalf("expected route reset: %#v", obj)
109
+	}
110
+	condition := obj.Status.Ingress[0].Conditions[0]
111
+	if condition.LastTransitionTime == nil || *condition.LastTransitionTime != now || condition.Status != kapi.ConditionTrue || condition.Reason != "" {
112
+		t.Fatalf("unexpected condition: %#v", condition)
113
+	}
114
+	if v, ok := admitter.expected.Peek(types.UID("uid1")); !ok || !reflect.DeepEqual(v, now.Time) {
115
+		t.Fatalf("did not record last modification time: %#v %#v", admitter.expected, v)
116
+	}
117
+}
118
+
119
+func TestStatusBackoffOnConflict(t *testing.T) {
120
+	now := unversioned.Now()
121
+	nowFn = func() unversioned.Time { return now }
122
+	touched := unversioned.Time{Time: now.Add(-time.Minute)}
123
+	p := &fakePlugin{}
124
+	c := testclient.NewSimpleFake(&(errors.NewConflict(kapi.Resource("Route"), "route1", nil).(*errors.StatusError).ErrStatus))
125
+	admitter := NewStatusAdmitter(p, c, "test")
126
+	err := admitter.HandleRoute(watch.Added, &routeapi.Route{
127
+		ObjectMeta: kapi.ObjectMeta{Name: "route1", Namespace: "default", UID: types.UID("uid1")},
128
+		Spec:       routeapi.RouteSpec{Host: "route1.test.local"},
129
+		Status: routeapi.RouteStatus{
130
+			Ingress: []routeapi.RouteIngress{
131
+				{
132
+					Host:       "route2.test.local",
133
+					RouterName: "test",
134
+					Conditions: []routeapi.RouteIngressCondition{
135
+						{
136
+							Type:               routeapi.RouteAdmitted,
137
+							Status:             kapi.ConditionFalse,
138
+							LastTransitionTime: &touched,
139
+						},
140
+					},
141
+				},
142
+			},
143
+		},
144
+	})
145
+	if len(c.Actions()) != 1 {
146
+		t.Fatalf("unexpected actions: %#v", c.Actions())
147
+	}
148
+	action := c.Actions()[0]
149
+	if action.GetVerb() != "update" || action.GetResource() != "routes" || action.GetSubresource() != "status" {
150
+		t.Fatalf("unexpected action: %#v", action)
151
+	}
152
+	obj := c.Actions()[0].(ktestclient.UpdateAction).GetObject().(*routeapi.Route)
153
+	if len(obj.Status.Ingress) != 1 && obj.Status.Ingress[0].Host != "route1.test.local" {
154
+		t.Fatalf("expected route reset: %#v", obj)
155
+	}
156
+	condition := obj.Status.Ingress[0].Conditions[0]
157
+	if condition.LastTransitionTime == nil || *condition.LastTransitionTime != now || condition.Status != kapi.ConditionTrue || condition.Reason != "" {
158
+		t.Fatalf("unexpected condition: %#v", condition)
159
+	}
160
+
161
+	if err == nil {
162
+		t.Fatalf("unexpected non-error: %#v", admitter.expected)
163
+	}
164
+	if v, ok := admitter.expected.Peek(types.UID("uid1")); !ok || !reflect.DeepEqual(v, time.Time{}) {
165
+		t.Fatalf("expected empty time: %#v", v)
166
+	}
167
+}
168
+
169
+func TestStatusRecordRejection(t *testing.T) {
170
+	now := unversioned.Now()
171
+	nowFn = func() unversioned.Time { return now }
172
+	touched := unversioned.Time{Time: now.Add(-time.Minute)}
173
+	p := &fakePlugin{}
174
+	c := testclient.NewSimpleFake(&routeapi.Route{})
175
+	admitter := NewStatusAdmitter(p, c, "test")
176
+	admitter.RecordRouteRejection(&routeapi.Route{
177
+		ObjectMeta: kapi.ObjectMeta{Name: "route1", Namespace: "default", UID: types.UID("uid1")},
178
+		Spec:       routeapi.RouteSpec{Host: "route1.test.local"},
179
+		Status: routeapi.RouteStatus{
180
+			Ingress: []routeapi.RouteIngress{
181
+				{
182
+					Host:       "route2.test.local",
183
+					RouterName: "test",
184
+					Conditions: []routeapi.RouteIngressCondition{
185
+						{
186
+							Type:               routeapi.RouteAdmitted,
187
+							Status:             kapi.ConditionFalse,
188
+							LastTransitionTime: &touched,
189
+						},
190
+					},
191
+				},
192
+			},
193
+		},
194
+	}, "Failed", "generic error")
195
+
196
+	if len(c.Actions()) != 1 {
197
+		t.Fatalf("unexpected actions: %#v", c.Actions())
198
+	}
199
+	action := c.Actions()[0]
200
+	if action.GetVerb() != "update" || action.GetResource() != "routes" || action.GetSubresource() != "status" {
201
+		t.Fatalf("unexpected action: %#v", action)
202
+	}
203
+	obj := c.Actions()[0].(ktestclient.UpdateAction).GetObject().(*routeapi.Route)
204
+	if len(obj.Status.Ingress) != 1 && obj.Status.Ingress[0].Host != "route1.test.local" {
205
+		t.Fatalf("expected route reset: %#v", obj)
206
+	}
207
+	condition := obj.Status.Ingress[0].Conditions[0]
208
+	if condition.LastTransitionTime == nil || *condition.LastTransitionTime != now || condition.Status != kapi.ConditionFalse || condition.Reason != "Failed" || condition.Message != "generic error" {
209
+		t.Fatalf("unexpected condition: %#v", condition)
210
+	}
211
+	if v, ok := admitter.expected.Peek(types.UID("uid1")); !ok || !reflect.DeepEqual(v, now.Time) {
212
+		t.Fatalf("expected empty time: %#v", v)
213
+	}
214
+}
215
+
216
+func TestStatusRecordRejectionConflict(t *testing.T) {
217
+	now := unversioned.Now()
218
+	nowFn = func() unversioned.Time { return now }
219
+	touched := unversioned.Time{Time: now.Add(-time.Minute)}
220
+	p := &fakePlugin{}
221
+	c := testclient.NewSimpleFake(&(errors.NewConflict(kapi.Resource("Route"), "route1", nil).(*errors.StatusError).ErrStatus))
222
+	admitter := NewStatusAdmitter(p, c, "test")
223
+	admitter.RecordRouteRejection(&routeapi.Route{
224
+		ObjectMeta: kapi.ObjectMeta{Name: "route1", Namespace: "default", UID: types.UID("uid1")},
225
+		Spec:       routeapi.RouteSpec{Host: "route1.test.local"},
226
+		Status: routeapi.RouteStatus{
227
+			Ingress: []routeapi.RouteIngress{
228
+				{
229
+					Host:       "route2.test.local",
230
+					RouterName: "test",
231
+					Conditions: []routeapi.RouteIngressCondition{
232
+						{
233
+							Type:               routeapi.RouteAdmitted,
234
+							Status:             kapi.ConditionFalse,
235
+							LastTransitionTime: &touched,
236
+						},
237
+					},
238
+				},
239
+			},
240
+		},
241
+	}, "Failed", "generic error")
242
+
243
+	if len(c.Actions()) != 1 {
244
+		t.Fatalf("unexpected actions: %#v", c.Actions())
245
+	}
246
+	action := c.Actions()[0]
247
+	if action.GetVerb() != "update" || action.GetResource() != "routes" || action.GetSubresource() != "status" {
248
+		t.Fatalf("unexpected action: %#v", action)
249
+	}
250
+	obj := c.Actions()[0].(ktestclient.UpdateAction).GetObject().(*routeapi.Route)
251
+	if len(obj.Status.Ingress) != 1 && obj.Status.Ingress[0].Host != "route1.test.local" {
252
+		t.Fatalf("expected route reset: %#v", obj)
253
+	}
254
+	condition := obj.Status.Ingress[0].Conditions[0]
255
+	if condition.LastTransitionTime == nil || *condition.LastTransitionTime != now || condition.Status != kapi.ConditionFalse || condition.Reason != "Failed" || condition.Message != "generic error" {
256
+		t.Fatalf("unexpected condition: %#v", condition)
257
+	}
258
+	if v, ok := admitter.expected.Peek(types.UID("uid1")); !ok || !reflect.DeepEqual(v, time.Time{}) {
259
+		t.Fatalf("expected empty time: %#v", v)
260
+	}
261
+}
262
+
263
+func TestFindOrCreateIngress(t *testing.T) {
264
+	route := &routeapi.Route{
265
+		Status: routeapi.RouteStatus{
266
+			Ingress: []routeapi.RouteIngress{
267
+				{
268
+					RouterName: "bar",
269
+					Conditions: []routeapi.RouteIngressCondition{
270
+						{
271
+							Reason: "bar",
272
+						},
273
+					},
274
+				},
275
+				{
276
+					RouterName: "foo",
277
+					Conditions: []routeapi.RouteIngressCondition{
278
+						{
279
+							Reason: "foo1",
280
+						},
281
+					},
282
+				},
283
+				{
284
+					RouterName: "baz",
285
+					Conditions: []routeapi.RouteIngressCondition{
286
+						{
287
+							Reason: "baz",
288
+						},
289
+					},
290
+				},
291
+				{
292
+					RouterName: "foo",
293
+					Conditions: []routeapi.RouteIngressCondition{
294
+						{
295
+							Reason: "foo2",
296
+						},
297
+					},
298
+				},
299
+			},
300
+		},
301
+	}
302
+
303
+	routerName := "foo"
304
+	ingress, changed := findOrCreateIngress(route, routerName)
305
+	if !changed {
306
+		t.Errorf("expected the route list to be changed: %#v", route.Status.Ingress)
307
+	}
308
+	if ingress.RouterName != routerName {
309
+		t.Errorf("returned ingress had router name %s but expected %s", ingress.RouterName, routerName)
310
+	}
311
+	t.Logf("routes: %#v", route.Status.Ingress)
312
+}
... ...
@@ -23,12 +23,27 @@ func HostForRoute(route *routeapi.Route) string {
23 23
 type HostToRouteMap map[string][]*routeapi.Route
24 24
 type RouteToHostMap map[string]string
25 25
 
26
+// RejectionRecorder is an object capable of recording why a route was rejected
27
+type RejectionRecorder interface {
28
+	RecordRouteRejection(route *routeapi.Route, reason, message string)
29
+}
30
+
31
+var LogRejections = logRecorder{}
32
+
33
+type logRecorder struct{}
34
+
35
+func (_ logRecorder) RecordRouteRejection(route *routeapi.Route, reason, message string) {
36
+	glog.V(4).Infof("Rejected route %s: %s: %s", route.Name, reason, message)
37
+}
38
+
26 39
 // UniqueHost implements the router.Plugin interface to provide
27 40
 // a template based, backend-agnostic router.
28 41
 type UniqueHost struct {
29 42
 	plugin       router.Plugin
30 43
 	hostForRoute RouteHostFunc
31 44
 
45
+	recorder RejectionRecorder
46
+
32 47
 	hostToRoute HostToRouteMap
33 48
 	routeToHost RouteToHostMap
34 49
 	// nil means different than empty
... ...
@@ -36,12 +51,15 @@ type UniqueHost struct {
36 36
 }
37 37
 
38 38
 // NewUniqueHost creates a plugin wrapper that ensures only unique routes are passed into
39
-// the underlying plugin.
40
-func NewUniqueHost(plugin router.Plugin, fn RouteHostFunc) *UniqueHost {
39
+// the underlying plugin. Recorder is an interface for indicating why a route was
40
+// rejected.
41
+func NewUniqueHost(plugin router.Plugin, fn RouteHostFunc, recorder RejectionRecorder) *UniqueHost {
41 42
 	return &UniqueHost{
42 43
 		plugin:       plugin,
43 44
 		hostForRoute: fn,
44 45
 
46
+		recorder: recorder,
47
+
45 48
 		hostToRoute: make(HostToRouteMap),
46 49
 		routeToHost: make(RouteToHostMap),
47 50
 	}
... ...
@@ -81,6 +99,7 @@ func (p *UniqueHost) HandleRoute(eventType watch.EventType, route *routeapi.Rout
81 81
 	host := p.hostForRoute(route)
82 82
 	if len(host) == 0 {
83 83
 		glog.V(4).Infof("Route %s has no host value", routeName)
84
+		p.recorder.RecordRouteRejection(route, "NoHostValue", "no host value was defined for the route")
84 85
 		return nil
85 86
 	}
86 87
 	route.Spec.Host = host
... ...
@@ -97,14 +116,17 @@ func (p *UniqueHost) HandleRoute(eventType watch.EventType, route *routeapi.Rout
97 97
 				if old[i].Spec.Path == route.Spec.Path {
98 98
 					if old[i].CreationTimestamp.Before(route.CreationTimestamp) {
99 99
 						glog.V(4).Infof("Route %s cannot take %s from %s", routeName, host, routeNameKey(oldest))
100
-						return fmt.Errorf("route %s holds %s and is older than %s", routeNameKey(oldest), host, key)
100
+						err := fmt.Errorf("route %s already exposes %s and is older", oldest.Name, host)
101
+						p.recorder.RecordRouteRejection(route, "HostAlreadyClaimed", err.Error())
102
+						return err
101 103
 					}
102 104
 					added = true
103 105
 					if old[i].Namespace == route.Namespace && old[i].Name == route.Name {
104 106
 						old[i] = route
105 107
 						break
106 108
 					}
107
-					glog.V(4).Infof("Route %s will replace path %s from %s because it is older", routeName, route.Spec.Path, routeNameKey(old[i]))
109
+					glog.V(4).Infof("route %s will replace path %s from %s because it is older", routeName, route.Spec.Path, old[i].Name)
110
+					p.recorder.RecordRouteRejection(old[i], "HostAlreadyClaimed", fmt.Sprintf("replaced by older route %s", route.Name))
108 111
 					p.plugin.HandleRoute(watch.Deleted, old[i])
109 112
 					old[i] = route
110 113
 				}
... ...
@@ -119,11 +141,14 @@ func (p *UniqueHost) HandleRoute(eventType watch.EventType, route *routeapi.Rout
119 119
 		} else {
120 120
 			if oldest.CreationTimestamp.Before(route.CreationTimestamp) {
121 121
 				glog.V(4).Infof("Route %s cannot take %s from %s", routeName, host, routeNameKey(oldest))
122
-				return fmt.Errorf("route %s holds %s and is older than %s", routeNameKey(oldest), host, key)
122
+				err := fmt.Errorf("another route holds %s and is older than %s", host, route.Name)
123
+				p.recorder.RecordRouteRejection(route, "HostAlreadyClaimed", err.Error())
124
+				return err
123 125
 			}
124 126
 
125 127
 			glog.V(4).Infof("Route %s is reclaiming %s from namespace %s", routeName, host, oldest.Namespace)
126 128
 			for i := range old {
129
+				p.recorder.RecordRouteRejection(old[i], "HostAlreadyClaimed", fmt.Sprintf("namespace %s owns hostname %s", oldest.Namespace, host))
127 130
 				p.plugin.HandleRoute(watch.Deleted, old[i])
128 131
 			}
129 132
 			p.hostToRoute[host] = []*routeapi.Route{route}
... ...
@@ -204,7 +204,7 @@ func TestHandleEndpoints(t *testing.T) {
204 204
 	templatePlugin := newDefaultTemplatePlugin(router, true)
205 205
 	// TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from
206 206
 	// here
207
-	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute)
207
+	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute, controller.LogRejections)
208 208
 
209 209
 	for _, tc := range testCases {
210 210
 		plugin.HandleEndpoints(tc.eventType, tc.endpoints)
... ...
@@ -315,7 +315,7 @@ func TestHandleTCPEndpoints(t *testing.T) {
315 315
 	templatePlugin := newDefaultTemplatePlugin(router, false)
316 316
 	// TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from
317 317
 	// here
318
-	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute)
318
+	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute, controller.LogRejections)
319 319
 
320 320
 	for _, tc := range testCases {
321 321
 		plugin.HandleEndpoints(tc.eventType, tc.endpoints)
... ...
@@ -340,13 +340,28 @@ func TestHandleTCPEndpoints(t *testing.T) {
340 340
 	}
341 341
 }
342 342
 
343
+type rejection struct {
344
+	route   *routeapi.Route
345
+	reason  string
346
+	message string
347
+}
348
+
349
+type fakeRejections struct {
350
+	rejections []rejection
351
+}
352
+
353
+func (r *fakeRejections) RecordRouteRejection(route *routeapi.Route, reason, message string) {
354
+	r.rejections = append(r.rejections, rejection{route: route, reason: reason, message: message})
355
+}
356
+
343 357
 // TestHandleRoute test route watch events
344 358
 func TestHandleRoute(t *testing.T) {
359
+	rejections := &fakeRejections{}
345 360
 	router := newTestRouter(make(map[string]ServiceUnit))
346 361
 	templatePlugin := newDefaultTemplatePlugin(router, true)
347 362
 	// TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from
348 363
 	// here
349
-	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute)
364
+	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute, rejections)
350 365
 
351 366
 	original := unversioned.Time{Time: time.Now()}
352 367
 
... ...
@@ -388,6 +403,10 @@ func TestHandleRoute(t *testing.T) {
388 388
 		}
389 389
 	}
390 390
 
391
+	if len(rejections.rejections) > 0 {
392
+		t.Fatalf("did not expect a recorded rejection: %#v", rejections)
393
+	}
394
+
391 395
 	// attempt to add a second route with a newer time, verify it is ignored
392 396
 	duplicateRoute := &routeapi.Route{
393 397
 		ObjectMeta: kapi.ObjectMeta{
... ...
@@ -411,6 +430,13 @@ func TestHandleRoute(t *testing.T) {
411 411
 	if r, ok := plugin.RoutesForHost("www.example.com"); !ok || r[0].Name != "test" {
412 412
 		t.Fatalf("unexpected claimed routes: %#v", r)
413 413
 	}
414
+	if len(rejections.rejections) != 1 ||
415
+		rejections.rejections[0].route.Name != "dupe" ||
416
+		rejections.rejections[0].reason != "HostAlreadyClaimed" ||
417
+		rejections.rejections[0].message != "route test already exposes www.example.com and is older" {
418
+		t.Fatalf("did not record rejection: %#v", rejections)
419
+	}
420
+	rejections.rejections = nil
414 421
 
415 422
 	// attempt to remove the second route that is not being used, verify it is ignored
416 423
 	if err := plugin.HandleRoute(watch.Deleted, duplicateRoute); err == nil {
... ...
@@ -425,6 +451,13 @@ func TestHandleRoute(t *testing.T) {
425 425
 	if r, ok := plugin.RoutesForHost("www.example.com"); !ok || r[0].Name != "test" {
426 426
 		t.Fatalf("unexpected claimed routes: %#v", r)
427 427
 	}
428
+	if len(rejections.rejections) != 1 ||
429
+		rejections.rejections[0].route.Name != "dupe" ||
430
+		rejections.rejections[0].reason != "HostAlreadyClaimed" ||
431
+		rejections.rejections[0].message != "route test already exposes www.example.com and is older" {
432
+		t.Fatalf("did not record rejection: %#v", rejections)
433
+	}
434
+	rejections.rejections = nil
428 435
 
429 436
 	// add a second route with an older time, verify it takes effect
430 437
 	duplicateRoute.CreationTimestamp = unversioned.Time{Time: original.Add(-time.Hour)}
... ...
@@ -441,6 +474,13 @@ func TestHandleRoute(t *testing.T) {
441 441
 	if _, ok := actualSU.ServiceAliasConfigs[router.routeKey(route)]; ok {
442 442
 		t.Errorf("unexpected service alias config %s", router.routeKey(route))
443 443
 	}
444
+	if len(rejections.rejections) != 1 ||
445
+		rejections.rejections[0].route.Name != "test" ||
446
+		rejections.rejections[0].reason != "HostAlreadyClaimed" ||
447
+		rejections.rejections[0].message != "replaced by older route dupe" {
448
+		t.Fatalf("did not record rejection: %#v", rejections)
449
+	}
450
+	rejections.rejections = nil
444 451
 
445 452
 	//mod
446 453
 	route.Spec.Host = "www.example2.com"
... ...
@@ -467,6 +507,9 @@ func TestHandleRoute(t *testing.T) {
467 467
 	if plugin.HostLen() != 1 {
468 468
 		t.Fatalf("did not clear claimed route: %#v", plugin)
469 469
 	}
470
+	if len(rejections.rejections) != 0 {
471
+		t.Fatalf("unexpected rejection: %#v", rejections)
472
+	}
470 473
 
471 474
 	//delete
472 475
 	if err := plugin.HandleRoute(watch.Deleted, route); err != nil {
... ...
@@ -488,6 +531,9 @@ func TestHandleRoute(t *testing.T) {
488 488
 	if plugin.HostLen() != 0 {
489 489
 		t.Errorf("did not clear claimed route: %#v", plugin)
490 490
 	}
491
+	if len(rejections.rejections) != 0 {
492
+		t.Fatalf("unexpected rejection: %#v", rejections)
493
+	}
491 494
 }
492 495
 
493 496
 func TestNamespaceScopingFromEmpty(t *testing.T) {
... ...
@@ -495,7 +541,7 @@ func TestNamespaceScopingFromEmpty(t *testing.T) {
495 495
 	templatePlugin := newDefaultTemplatePlugin(router, true)
496 496
 	// TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from
497 497
 	// here
498
-	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute)
498
+	plugin := controller.NewUniqueHost(templatePlugin, controller.HostForRoute, controller.LogRejections)
499 499
 
500 500
 	// no namespaces allowed
501 501
 	plugin.HandleNamespaces(sets.String{})
... ...
@@ -256,6 +256,12 @@ items:
256 256
     verbs:
257 257
     - get
258 258
     - update
259
+  - apiGroups: null
260
+    attributeRestrictions: null
261
+    resources:
262
+    - routes/status
263
+    verbs:
264
+    - update
259 265
 - apiVersion: v1
260 266
   kind: ClusterRole
261 267
   metadata:
... ...
@@ -671,6 +677,12 @@ items:
671 671
     verbs:
672 672
     - list
673 673
     - watch
674
+  - apiGroups: null
675
+    attributeRestrictions: null
676
+    resources:
677
+    - routes/status
678
+    verbs:
679
+    - update
674 680
 - apiVersion: v1
675 681
   kind: ClusterRole
676 682
   metadata:
... ...
@@ -126,6 +126,11 @@ func (s *TestHttpService) handleRouteList(w http.ResponseWriter, r *http.Request
126 126
 	fmt.Fprint(w, "{}")
127 127
 }
128 128
 
129
+// handleRouteCalls handles calls to /osapi/v1/routes/* and returns whatever the client sent
130
+func (s *TestHttpService) handleRouteCalls(w http.ResponseWriter, r *http.Request) {
131
+	fmt.Fprint(w, "{}")
132
+}
133
+
129 134
 // handleEndpointWatch handles calls to /api/v1beta1/watch/endpoints and uses the endpoint channel to simulate watch events
130 135
 func (s *TestHttpService) handleEndpointWatch(w http.ResponseWriter, r *http.Request) {
131 136
 	io.WriteString(w, <-s.EndpointChannel)
... ...
@@ -182,6 +187,7 @@ func (s *TestHttpService) startMaster() error {
182 182
 		masterServer.HandleFunc(fmt.Sprintf("/api/%s/endpoints", version), s.handleEndpointList)
183 183
 		masterServer.HandleFunc(fmt.Sprintf("/api/%s/watch/endpoints", version), s.handleEndpointWatch)
184 184
 		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/routes", version), s.handleRouteList)
185
+		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/namespaces/", version), s.handleRouteCalls)
185 186
 		masterServer.HandleFunc(fmt.Sprintf("/oapi/%s/watch/routes", version), s.handleRouteWatch)
186 187
 	}
187 188