diff --git a/api/swagger-spec/v1beta1.json b/api/swagger-spec/v1beta1.json index b3aaf167abf..11c49acf110 100644 --- a/api/swagger-spec/v1beta1.json +++ b/api/swagger-spec/v1beta1.json @@ -50,6 +50,22 @@ "summary": "list objects of kind Endpoints", "nickname": "listEndpoints", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -57,6 +73,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -103,11 +135,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.EndpointsList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Endpoints", + "summary": "watch individual changes to a list of Endpoints", "nickname": "watchEndpointslist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -115,6 +163,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -281,9 +345,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Endpoints", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Endpoints", + "summary": "watch changes to an object of kind Endpoints", "nickname": "watchEndpoints", "parameters": [ { @@ -294,6 +358,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -301,6 +381,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -322,6 +418,22 @@ "summary": "list objects of kind Event", "nickname": "listEvent", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -329,6 +441,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -375,11 +503,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.EventList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Event", + "summary": "watch individual changes to a list of Event", "nickname": "watchEventlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -387,6 +531,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -545,9 +705,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Event", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Event", + "summary": "watch changes to an object of kind Event", "nickname": "watchEvent", "parameters": [ { @@ -558,6 +718,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -565,6 +741,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -586,6 +778,22 @@ "summary": "list objects of kind LimitRange", "nickname": "listLimitRange", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -593,6 +801,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -639,11 +863,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.LimitRangeList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of LimitRange", + "summary": "watch individual changes to a list of LimitRange", "nickname": "watchLimitRangelist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -651,6 +891,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -809,9 +1065,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.LimitRange", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular LimitRange", + "summary": "watch changes to an object of kind LimitRange", "nickname": "watchLimitRange", "parameters": [ { @@ -822,6 +1078,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -829,6 +1101,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -849,7 +1137,40 @@ "method": "GET", "summary": "list objects of kind Node", "nickname": "listNode", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -886,11 +1207,44 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.MinionList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Node", + "summary": "watch individual changes to a list of Node", "nickname": "watchNodelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1023,9 +1377,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Minion", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Node", + "summary": "watch changes to an object of kind Node", "nickname": "watchNode", "parameters": [ { @@ -1035,6 +1389,38 @@ "description": "name of the Node", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1271,7 +1657,40 @@ "method": "GET", "summary": "list objects of kind Namespace", "nickname": "listNamespace", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1308,11 +1727,44 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.NamespaceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Namespace", + "summary": "watch individual changes to a list of Namespace", "nickname": "watchNamespacelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1445,9 +1897,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Namespace", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Namespace", + "summary": "watch changes to an object of kind Namespace", "nickname": "watchNamespace", "parameters": [ { @@ -1457,6 +1909,38 @@ "description": "name of the Namespace", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1549,7 +2033,40 @@ "method": "GET", "summary": "list objects of kind Node", "nickname": "listNode", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1586,11 +2103,44 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.MinionList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Node", + "summary": "watch individual changes to a list of Node", "nickname": "watchNodelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1723,9 +2273,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Minion", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Node", + "summary": "watch changes to an object of kind Node", "nickname": "watchNode", "parameters": [ { @@ -1735,6 +2285,38 @@ "description": "name of the Node", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1972,6 +2554,22 @@ "summary": "list objects of kind Pod", "nickname": "listPod", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -1979,6 +2577,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2025,11 +2639,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.PodList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Pod", + "summary": "watch individual changes to a list of Pod", "nickname": "watchPodlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2037,6 +2667,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2203,9 +2849,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Pod", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Pod", + "summary": "watch changes to an object of kind Pod", "nickname": "watchPod", "parameters": [ { @@ -2216,6 +2862,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2223,6 +2885,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2612,6 +3290,22 @@ "summary": "list objects of kind ReplicationController", "nickname": "listReplicationController", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2619,6 +3313,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2665,11 +3375,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.ReplicationControllerList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ReplicationController", + "summary": "watch individual changes to a list of ReplicationController", "nickname": "watchReplicationControllerlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2677,6 +3403,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2843,9 +3585,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.ReplicationController", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ReplicationController", + "summary": "watch changes to an object of kind ReplicationController", "nickname": "watchReplicationController", "parameters": [ { @@ -2856,6 +3598,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2863,6 +3621,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2884,6 +3658,22 @@ "summary": "list objects of kind ResourceQuota", "nickname": "listResourceQuota", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2891,6 +3681,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2937,11 +3743,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.ResourceQuotaList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ResourceQuota", + "summary": "watch individual changes to a list of ResourceQuota", "nickname": "watchResourceQuotalist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2949,6 +3771,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3115,9 +3953,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.ResourceQuota", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ResourceQuota", + "summary": "watch changes to an object of kind ResourceQuota", "nickname": "watchResourceQuota", "parameters": [ { @@ -3128,6 +3966,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3135,6 +3989,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3200,6 +4070,22 @@ "summary": "list objects of kind Secret", "nickname": "listSecret", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3207,6 +4093,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3253,11 +4155,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.SecretList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Secret", + "summary": "watch individual changes to a list of Secret", "nickname": "watchSecretlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3265,6 +4183,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3423,9 +4357,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Secret", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Secret", + "summary": "watch changes to an object of kind Secret", "nickname": "watchSecret", "parameters": [ { @@ -3436,6 +4370,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3443,6 +4393,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3464,6 +4430,22 @@ "summary": "list objects of kind Service", "nickname": "listService", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3471,6 +4453,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3517,11 +4515,27 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.ServiceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Service", + "summary": "watch individual changes to a list of Service", "nickname": "watchServicelist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3529,6 +4543,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3687,9 +4717,9 @@ "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.Service", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Service", + "summary": "watch changes to an object of kind Service", "nickname": "watchService", "parameters": [ { @@ -3700,6 +4730,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3707,6 +4753,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -4008,6 +5070,10 @@ } ], "models": { + "*json.watchEvent": { + "id": "*json.watchEvent", + "properties": {} + }, "*v1beta1.DeleteOptions": { "id": "*v1beta1.DeleteOptions", "properties": {} @@ -4279,6 +5345,22 @@ } } }, + "v1beta1.EndpointAddress": { + "id": "v1beta1.EndpointAddress", + "required": [ + "IP" + ], + "properties": { + "IP": { + "type": "string", + "description": "IP address of the endpoint" + }, + "targetRef": { + "$ref": "v1beta1.ObjectReference", + "description": "reference to object providing the endpoint" + } + } + }, "v1beta1.EndpointObjectReference": { "id": "v1beta1.EndpointObjectReference", "required": [ @@ -4319,10 +5401,51 @@ } } }, + "v1beta1.EndpointPort": { + "id": "v1beta1.EndpointPort", + "required": [ + "port" + ], + "properties": { + "name": { + "type": "string", + "description": "name of this port" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "port number of the endpoint" + }, + "protocol": { + "$ref": "v1beta1.Protocol", + "description": "protocol for this port; must be UDP or TCP; TCP if unspecified" + } + } + }, + "v1beta1.EndpointSubset": { + "id": "v1beta1.EndpointSubset", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "v1beta1.EndpointAddress" + }, + "description": "IP addresses which offer the related ports" + }, + "ports": { + "type": "array", + "items": { + "$ref": "v1beta1.EndpointPort" + }, + "description": "port numbers available on the related IP addresses" + } + } + }, "v1beta1.Endpoints": { "id": "v1beta1.Endpoints", "required": [ - "endpoints" + "endpoints", + "subsets" ], "properties": { "annotations": { @@ -4346,7 +5469,7 @@ "items": { "type": "string" }, - "description": "list of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909" + "description": "first set of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909" }, "generateName": { "type": "string", @@ -4366,7 +5489,7 @@ }, "protocol": { "$ref": "v1beta1.Protocol", - "description": "IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified" + "description": "IP protocol for the first set of endpoint ports; must be UDP or TCP; TCP if unspecified" }, "resourceVersion": { "$ref": "uint64", @@ -4376,6 +5499,13 @@ "type": "string", "description": "URL for the object; populated by the system, read-only" }, + "subsets": { + "type": "array", + "items": { + "$ref": "v1beta1.EndpointSubset" + }, + "description": "sets of addresses and ports that comprise a service" + }, "targetRefs": { "type": "array", "items": { @@ -5308,13 +6438,43 @@ "id": "v1beta1.NodeSystemInfo", "required": [ "machineID", - "systemUUID" + "systemUUID", + "bootID", + "kernelVersion", + "osImage", + "containerRuntimeVersion", + "kubeletVersion", + "KubeProxyVersion" ], "properties": { + "KubeProxyVersion": { + "type": "string", + "description": "Kube-proxy version reported by the node" + }, + "bootID": { + "type": "string", + "description": "boot id is the boot-id reported by the node" + }, + "containerRuntimeVersion": { + "type": "string", + "description": "Container runtime version reported by the node through runtime remote API (e.g. docker://1.5.0)" + }, + "kernelVersion": { + "type": "string", + "description": "Kernel version reported by the node from 'uname -r' (e.g. 3.16.0-0.bpo.4-amd64)" + }, + "kubeletVersion": { + "type": "string", + "description": "Kubelet version reported by the node" + }, "machineID": { "type": "string", "description": "machine id is the machine-id reported by the node" }, + "osImage": { + "type": "string", + "description": "OS image used reported by the node from /etc/os-release (e.g. Debian GNU/Linux 7 (wheezy))" + }, "systemUUID": { "type": "string", "description": "system uuid is the system-uuid reported by the node" @@ -6049,7 +7209,8 @@ "id": "v1beta1.Service", "required": [ "port", - "selector" + "selector", + "ports" ], "properties": { "annotations": { @@ -6101,10 +7262,21 @@ "format": "int32", "description": "port exposed by the service" }, + "portName": { + "type": "string", + "description": "the name of the first port; optional" + }, "portalIP": { "type": "string", "description": "IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required" }, + "ports": { + "type": "array", + "items": { + "$ref": "v1beta1.ServicePort" + }, + "description": "ports to be exposed on the service" + }, "protocol": { "$ref": "v1beta1.Protocol", "description": "protocol for port; must be UDP or TCP; TCP if unspecified" @@ -6210,6 +7382,34 @@ } } }, + "v1beta1.ServicePort": { + "id": "v1beta1.ServicePort", + "required": [ + "name", + "protocol", + "port", + "containerPort" + ], + "properties": { + "containerPort": { + "type": "string", + "description": "the port to access on the containers belonging to pods targeted by the service; defaults to the service port" + }, + "name": { + "type": "string", + "description": "the name of this port; optional if only one port is defined" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "the port number that is exposed" + }, + "protocol": { + "$ref": "v1beta1.Protocol", + "description": "the protocol used by this port; must be UDP or TCP; TCP if unspecified" + } + } + }, "v1beta1.TCPSocketAction": { "id": "v1beta1.TCPSocketAction", "properties": { diff --git a/api/swagger-spec/v1beta2.json b/api/swagger-spec/v1beta2.json index 441d9769485..65ca9aad1bb 100644 --- a/api/swagger-spec/v1beta2.json +++ b/api/swagger-spec/v1beta2.json @@ -50,6 +50,22 @@ "summary": "list objects of kind Endpoints", "nickname": "listEndpoints", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -57,6 +73,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -103,11 +135,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.EndpointsList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Endpoints", + "summary": "watch individual changes to a list of Endpoints", "nickname": "watchEndpointslist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -115,6 +163,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -281,9 +345,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Endpoints", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Endpoints", + "summary": "watch changes to an object of kind Endpoints", "nickname": "watchEndpoints", "parameters": [ { @@ -294,6 +358,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -301,6 +381,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -322,6 +418,22 @@ "summary": "list objects of kind Event", "nickname": "listEvent", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -329,6 +441,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -375,11 +503,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.EventList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Event", + "summary": "watch individual changes to a list of Event", "nickname": "watchEventlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -387,6 +531,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -545,9 +705,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Event", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Event", + "summary": "watch changes to an object of kind Event", "nickname": "watchEvent", "parameters": [ { @@ -558,6 +718,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -565,6 +741,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -586,6 +778,22 @@ "summary": "list objects of kind LimitRange", "nickname": "listLimitRange", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -593,6 +801,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -639,11 +863,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.LimitRangeList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of LimitRange", + "summary": "watch individual changes to a list of LimitRange", "nickname": "watchLimitRangelist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -651,6 +891,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -809,9 +1065,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.LimitRange", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular LimitRange", + "summary": "watch changes to an object of kind LimitRange", "nickname": "watchLimitRange", "parameters": [ { @@ -822,6 +1078,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -829,6 +1101,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -849,7 +1137,40 @@ "method": "GET", "summary": "list objects of kind Node", "nickname": "listNode", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -886,11 +1207,44 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.MinionList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Node", + "summary": "watch individual changes to a list of Node", "nickname": "watchNodelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1023,9 +1377,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Minion", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Node", + "summary": "watch changes to an object of kind Node", "nickname": "watchNode", "parameters": [ { @@ -1035,6 +1389,38 @@ "description": "name of the Node", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1271,7 +1657,40 @@ "method": "GET", "summary": "list objects of kind Namespace", "nickname": "listNamespace", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1308,11 +1727,44 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.NamespaceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Namespace", + "summary": "watch individual changes to a list of Namespace", "nickname": "watchNamespacelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1445,9 +1897,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Namespace", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Namespace", + "summary": "watch changes to an object of kind Namespace", "nickname": "watchNamespace", "parameters": [ { @@ -1457,6 +1909,38 @@ "description": "name of the Namespace", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1549,7 +2033,40 @@ "method": "GET", "summary": "list objects of kind Node", "nickname": "listNode", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1586,11 +2103,44 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.MinionList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Node", + "summary": "watch individual changes to a list of Node", "nickname": "watchNodelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1723,9 +2273,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Minion", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Node", + "summary": "watch changes to an object of kind Node", "nickname": "watchNode", "parameters": [ { @@ -1735,6 +2285,38 @@ "description": "name of the Node", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1972,6 +2554,22 @@ "summary": "list objects of kind Pod", "nickname": "listPod", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -1979,6 +2577,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2025,11 +2639,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.PodList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Pod", + "summary": "watch individual changes to a list of Pod", "nickname": "watchPodlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2037,6 +2667,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2203,9 +2849,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Pod", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Pod", + "summary": "watch changes to an object of kind Pod", "nickname": "watchPod", "parameters": [ { @@ -2216,6 +2862,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2223,6 +2885,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2612,6 +3290,22 @@ "summary": "list objects of kind ReplicationController", "nickname": "listReplicationController", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2619,6 +3313,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2665,11 +3375,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.ReplicationControllerList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ReplicationController", + "summary": "watch individual changes to a list of ReplicationController", "nickname": "watchReplicationControllerlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2677,6 +3403,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2843,9 +3585,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.ReplicationController", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ReplicationController", + "summary": "watch changes to an object of kind ReplicationController", "nickname": "watchReplicationController", "parameters": [ { @@ -2856,6 +3598,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2863,6 +3621,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2884,6 +3658,22 @@ "summary": "list objects of kind ResourceQuota", "nickname": "listResourceQuota", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2891,6 +3681,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2937,11 +3743,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.ResourceQuotaList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ResourceQuota", + "summary": "watch individual changes to a list of ResourceQuota", "nickname": "watchResourceQuotalist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -2949,6 +3771,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3115,9 +3953,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.ResourceQuota", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ResourceQuota", + "summary": "watch changes to an object of kind ResourceQuota", "nickname": "watchResourceQuota", "parameters": [ { @@ -3128,6 +3966,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3135,6 +3989,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3200,6 +4070,22 @@ "summary": "list objects of kind Secret", "nickname": "listSecret", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3207,6 +4093,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3253,11 +4155,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.SecretList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Secret", + "summary": "watch individual changes to a list of Secret", "nickname": "watchSecretlist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3265,6 +4183,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3423,9 +4357,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Secret", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Secret", + "summary": "watch changes to an object of kind Secret", "nickname": "watchSecret", "parameters": [ { @@ -3436,6 +4370,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3443,6 +4393,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3464,6 +4430,22 @@ "summary": "list objects of kind Service", "nickname": "listService", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3471,6 +4453,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3517,11 +4515,27 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.ServiceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Service", + "summary": "watch individual changes to a list of Service", "nickname": "watchServicelist", "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3529,6 +4543,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3687,9 +4717,9 @@ "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.Service", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Service", + "summary": "watch changes to an object of kind Service", "nickname": "watchService", "parameters": [ { @@ -3700,6 +4730,22 @@ "required": true, "allowMultiple": false }, + { + "type": "string", + "paramType": "query", + "name": "fields", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labels", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, { "type": "string", "paramType": "query", @@ -3707,6 +4753,22 @@ "description": "object name and auth scope, such as for teams and projects", "required": false, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -4008,6 +5070,10 @@ } ], "models": { + "*json.watchEvent": { + "id": "*json.watchEvent", + "properties": {} + }, "*v1beta2.DeleteOptions": { "id": "*v1beta2.DeleteOptions", "properties": {} @@ -4279,6 +5345,22 @@ } } }, + "v1beta2.EndpointAddress": { + "id": "v1beta2.EndpointAddress", + "required": [ + "IP" + ], + "properties": { + "IP": { + "type": "string", + "description": "IP address of the endpoint" + }, + "targetRef": { + "$ref": "v1beta2.ObjectReference", + "description": "reference to object providing the endpoint" + } + } + }, "v1beta2.EndpointObjectReference": { "id": "v1beta2.EndpointObjectReference", "required": [ @@ -4319,10 +5401,51 @@ } } }, + "v1beta2.EndpointPort": { + "id": "v1beta2.EndpointPort", + "required": [ + "port" + ], + "properties": { + "name": { + "type": "string", + "description": "name of this port" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "port number of the endpoint" + }, + "protocol": { + "$ref": "v1beta2.Protocol", + "description": "protocol for this port; must be UDP or TCP; TCP if unspecified" + } + } + }, + "v1beta2.EndpointSubset": { + "id": "v1beta2.EndpointSubset", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "v1beta2.EndpointAddress" + }, + "description": "IP addresses which offer the related ports" + }, + "ports": { + "type": "array", + "items": { + "$ref": "v1beta2.EndpointPort" + }, + "description": "port numbers available on the related IP addresses" + } + } + }, "v1beta2.Endpoints": { "id": "v1beta2.Endpoints", "required": [ - "endpoints" + "endpoints", + "subsets" ], "properties": { "annotations": { @@ -4346,7 +5469,7 @@ "items": { "type": "string" }, - "description": "list of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909" + "description": "first set of endpoints corresponding to a service, of the form address:port, such as 10.10.1.1:1909" }, "generateName": { "type": "string", @@ -4366,7 +5489,7 @@ }, "protocol": { "$ref": "v1beta2.Protocol", - "description": "IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified" + "description": "IP protocol for the first set of endpoint ports; must be UDP or TCP; TCP if unspecified" }, "resourceVersion": { "$ref": "uint64", @@ -4376,6 +5499,13 @@ "type": "string", "description": "URL for the object; populated by the system, read-only" }, + "subsets": { + "type": "array", + "items": { + "$ref": "v1beta2.EndpointSubset" + }, + "description": "sets of addresses and ports that comprise a service" + }, "targetRefs": { "type": "array", "items": { @@ -5297,13 +6427,43 @@ "id": "v1beta2.NodeSystemInfo", "required": [ "machineID", - "systemUUID" + "systemUUID", + "bootID", + "kernelVersion", + "osImage", + "containerRuntimeVersion", + "kubeletVersion", + "KubeProxyVersion" ], "properties": { + "KubeProxyVersion": { + "type": "string", + "description": "Kube-proxy version reported by the node" + }, + "bootID": { + "type": "string", + "description": "boot id is the boot-id reported by the node" + }, + "containerRuntimeVersion": { + "type": "string", + "description": "Container runtime version reported by the node through runtime remote API (e.g. docker://1.5.0)" + }, + "kernelVersion": { + "type": "string", + "description": "Kernel version reported by the node from 'uname -r' (e.g. 3.16.0-0.bpo.4-amd64)" + }, + "kubeletVersion": { + "type": "string", + "description": "Kubelet version reported by the node" + }, "machineID": { "type": "string", "description": "machine id is the machine-id reported by the node" }, + "osImage": { + "type": "string", + "description": "OS image used reported by the node from /etc/os-release (e.g. Debian GNU/Linux 7 (wheezy))" + }, "systemUUID": { "type": "string", "description": "system uuid is the system-uuid reported by the node" @@ -6038,7 +7198,8 @@ "id": "v1beta2.Service", "required": [ "port", - "selector" + "selector", + "ports" ], "properties": { "annotations": { @@ -6090,10 +7251,21 @@ "format": "int32", "description": "port exposed by the service" }, + "portName": { + "type": "string", + "description": "the name of the first port; optional" + }, "portalIP": { "type": "string", "description": "IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required" }, + "ports": { + "type": "array", + "items": { + "$ref": "v1beta2.ServicePort" + }, + "description": "ports to be exposed on the service" + }, "protocol": { "$ref": "v1beta2.Protocol", "description": "protocol for port; must be UDP or TCP; TCP if unspecified" @@ -6199,6 +7371,34 @@ } } }, + "v1beta2.ServicePort": { + "id": "v1beta2.ServicePort", + "required": [ + "name", + "protocol", + "port", + "containerPort" + ], + "properties": { + "containerPort": { + "type": "string", + "description": "the port to access on the containers belonging to pods targeted by the service; defaults to the service port" + }, + "name": { + "type": "string", + "description": "the name of this port; optional if only one port is defined" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "the port number that is exposed" + }, + "protocol": { + "$ref": "v1beta2.Protocol", + "description": "the protocol used by this port; must be UDP or TCP; TCP if unspecified" + } + } + }, "v1beta2.TCPSocketAction": { "id": "v1beta2.TCPSocketAction", "properties": { diff --git a/api/swagger-spec/v1beta3.json b/api/swagger-spec/v1beta3.json index 78c5281d78a..32151078bed 100644 --- a/api/swagger-spec/v1beta3.json +++ b/api/swagger-spec/v1beta3.json @@ -57,6 +57,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -103,9 +135,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.EndpointsList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Endpoints", + "summary": "watch individual changes to a list of Endpoints", "nickname": "watchEndpointslist", "parameters": [ { @@ -115,6 +147,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -281,9 +345,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Endpoints", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Endpoints", + "summary": "watch changes to an object of kind Endpoints", "nickname": "watchEndpoints", "parameters": [ { @@ -301,6 +365,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -321,7 +417,40 @@ "method": "GET", "summary": "list objects of kind Endpoints", "nickname": "listEndpoints", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -336,11 +465,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.EndpointsList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Endpoints", + "summary": "watch individual changes to a list of Endpoints", "nickname": "watchEndpointslist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -367,6 +529,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -413,9 +607,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.EventList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Event", + "summary": "watch individual changes to a list of Event", "nickname": "watchEventlist", "parameters": [ { @@ -425,6 +619,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -583,9 +809,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Event", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Event", + "summary": "watch changes to an object of kind Event", "nickname": "watchEvent", "parameters": [ { @@ -603,6 +829,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -623,7 +881,40 @@ "method": "GET", "summary": "list objects of kind Event", "nickname": "listEvent", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -638,11 +929,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.EventList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Event", + "summary": "watch individual changes to a list of Event", "nickname": "watchEventlist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -669,6 +993,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -715,9 +1071,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.LimitRangeList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of LimitRange", + "summary": "watch individual changes to a list of LimitRange", "nickname": "watchLimitRangelist", "parameters": [ { @@ -727,6 +1083,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -885,9 +1273,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.LimitRange", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular LimitRange", + "summary": "watch changes to an object of kind LimitRange", "nickname": "watchLimitRange", "parameters": [ { @@ -905,6 +1293,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -925,7 +1345,40 @@ "method": "GET", "summary": "list objects of kind LimitRange", "nickname": "listLimitRange", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -940,11 +1393,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.LimitRangeList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of LimitRange", + "summary": "watch individual changes to a list of LimitRange", "nickname": "watchLimitRangelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -963,7 +1449,40 @@ "method": "GET", "summary": "list objects of kind Namespace", "nickname": "listNamespace", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1000,11 +1519,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.NamespaceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Namespace", + "summary": "watch individual changes to a list of Namespace", "nickname": "watchNamespacelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1137,9 +1689,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Namespace", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Namespace", + "summary": "watch changes to an object of kind Namespace", "nickname": "watchNamespace", "parameters": [ { @@ -1149,6 +1701,38 @@ "description": "name of the Namespace", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1241,7 +1825,40 @@ "method": "GET", "summary": "list objects of kind Node", "nickname": "listNode", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1278,11 +1895,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.NodeList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Node", + "summary": "watch individual changes to a list of Node", "nickname": "watchNodelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -1415,9 +2065,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Node", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Node", + "summary": "watch changes to an object of kind Node", "nickname": "watchNode", "parameters": [ { @@ -1427,6 +2077,38 @@ "description": "name of the Node", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1671,6 +2353,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1717,9 +2431,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.PodList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Pod", + "summary": "watch individual changes to a list of Pod", "nickname": "watchPodlist", "parameters": [ { @@ -1729,6 +2443,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -1895,9 +2641,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Pod", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Pod", + "summary": "watch changes to an object of kind Pod", "nickname": "watchPod", "parameters": [ { @@ -1915,6 +2661,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2223,7 +3001,40 @@ "method": "GET", "summary": "list objects of kind Pod", "nickname": "listPod", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -2238,11 +3049,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.PodList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Pod", + "summary": "watch individual changes to a list of Pod", "nickname": "watchPodlist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -2349,6 +3193,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2395,9 +3271,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ReplicationControllerList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ReplicationController", + "summary": "watch individual changes to a list of ReplicationController", "nickname": "watchReplicationControllerlist", "parameters": [ { @@ -2407,6 +3283,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2573,9 +3481,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ReplicationController", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ReplicationController", + "summary": "watch changes to an object of kind ReplicationController", "nickname": "watchReplicationController", "parameters": [ { @@ -2593,6 +3501,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2613,7 +3553,40 @@ "method": "GET", "summary": "list objects of kind ReplicationController", "nickname": "listReplicationController", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -2628,11 +3601,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ReplicationControllerList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ReplicationController", + "summary": "watch individual changes to a list of ReplicationController", "nickname": "watchReplicationControllerlist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -2659,6 +3665,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2705,9 +3743,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ResourceQuotaList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ResourceQuota", + "summary": "watch individual changes to a list of ResourceQuota", "nickname": "watchResourceQuotalist", "parameters": [ { @@ -2717,6 +3755,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2883,9 +3953,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ResourceQuota", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular ResourceQuota", + "summary": "watch changes to an object of kind ResourceQuota", "nickname": "watchResourceQuota", "parameters": [ { @@ -2903,6 +3973,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -2923,7 +4025,40 @@ "method": "GET", "summary": "list objects of kind ResourceQuota", "nickname": "listResourceQuota", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -2938,11 +4073,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ResourceQuotaList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of ResourceQuota", + "summary": "watch individual changes to a list of ResourceQuota", "nickname": "watchResourceQuotalist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -3013,6 +4181,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3059,9 +4259,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.SecretList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Secret", + "summary": "watch individual changes to a list of Secret", "nickname": "watchSecretlist", "parameters": [ { @@ -3071,6 +4271,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3229,9 +4461,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Secret", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Secret", + "summary": "watch changes to an object of kind Secret", "nickname": "watchSecret", "parameters": [ { @@ -3249,6 +4481,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3269,7 +4533,40 @@ "method": "GET", "summary": "list objects of kind Secret", "nickname": "listSecret", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -3284,11 +4581,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.SecretList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Secret", + "summary": "watch individual changes to a list of Secret", "nickname": "watchSecretlist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -3315,6 +4645,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3361,9 +4723,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ServiceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Service", + "summary": "watch individual changes to a list of Service", "nickname": "watchServicelist", "parameters": [ { @@ -3373,6 +4735,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3531,9 +4925,9 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.Service", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a particular Service", + "summary": "watch changes to an object of kind Service", "nickname": "watchService", "parameters": [ { @@ -3551,6 +4945,38 @@ "description": "object name and auth scope, such as for teams and projects", "required": true, "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false } ], "produces": [ @@ -3859,7 +5285,40 @@ "method": "GET", "summary": "list objects of kind Service", "nickname": "listService", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -3874,11 +5333,44 @@ "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.ServiceList", + "type": "*json.watchEvent", "method": "GET", - "summary": "watch a list of Service", + "summary": "watch individual changes to a list of Service", "nickname": "watchServicelist", - "parameters": [], + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "fieldSelector", + "description": "a selector to restrict the list of returned objects by their fields; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "labelSelector", + "description": "a selector to restrict the list of returned objects by their labels; defaults to everything", + "required": false, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "query", + "name": "resourceVersion", + "description": "when specified with a watch call, shows changes that occur after that particular version of a resource; defaults to changes from the beginning of history", + "required": false, + "allowMultiple": false + }, + { + "type": "bool", + "paramType": "query", + "name": "watch", + "description": "watch for changes to the described resources and return them as a stream of add, update, and remove notifications; specify resourceVersion", + "required": false, + "allowMultiple": false + } + ], "produces": [ "application/json" ], @@ -3890,6 +5382,10 @@ } ], "models": { + "*json.watchEvent": { + "id": "*json.watchEvent", + "properties": {} + }, "*v1beta3.DeleteOptions": { "id": "*v1beta3.DeleteOptions", "properties": {} @@ -4089,21 +5585,133 @@ } } }, - "v1beta3.Endpoint": { - "id": "v1beta3.Endpoint", + "v1beta3.ContainerState": { + "id": "v1beta3.ContainerState", + "properties": { + "running": { + "$ref": "v1beta3.ContainerStateRunning", + "description": "details about a running container" + }, + "termination": { + "$ref": "v1beta3.ContainerStateTerminated", + "description": "details about a terminated container" + }, + "waiting": { + "$ref": "v1beta3.ContainerStateWaiting", + "description": "details about a waiting container" + } + } + }, + "v1beta3.ContainerStateRunning": { + "id": "v1beta3.ContainerStateRunning", + "properties": { + "startedAt": { + "type": "string", + "description": "time at which the container was last (re-)started" + } + } + }, + "v1beta3.ContainerStateTerminated": { + "id": "v1beta3.ContainerStateTerminated", "required": [ - "ip", - "port" + "exitCode" ], "properties": { - "ip": { + "containerID": { "type": "string", - "description": "IP of this endpoint" + "description": "container's ID in the format 'docker://\u003ccontainer_id\u003e'" }, - "port": { + "exitCode": { "type": "integer", "format": "int32", - "description": "destination port of this endpoint" + "description": "exit status from the last termination of the container" + }, + "finishedAt": { + "type": "string", + "description": "time at which the container last terminated" + }, + "message": { + "type": "string", + "description": "message regarding the last termination of the container" + }, + "reason": { + "type": "string", + "description": "(brief) reason from the last termination of the container" + }, + "signal": { + "type": "integer", + "format": "int32", + "description": "signal from the last termination of the container" + }, + "startedAt": { + "type": "string", + "description": "time at which previous execution of the container started" + } + } + }, + "v1beta3.ContainerStateWaiting": { + "id": "v1beta3.ContainerStateWaiting", + "properties": { + "reason": { + "type": "string", + "description": "(brief) reason the container is not yet running, such as pulling its image" + } + } + }, + "v1beta3.ContainerStatus": { + "id": "v1beta3.ContainerStatus", + "required": [ + "name", + "ready", + "restartCount", + "image", + "imageID" + ], + "properties": { + "containerID": { + "type": "string", + "description": "container's ID in the format 'docker://\u003ccontainer_id\u003e'" + }, + "image": { + "type": "string", + "description": "image of the container" + }, + "imageID": { + "type": "string", + "description": "ID of the container's image" + }, + "lastState": { + "$ref": "v1beta3.ContainerState", + "description": "details about the container's last termination condition" + }, + "name": { + "type": "string", + "description": "name of the container; must be a DNS_LABEL and unique within the pod; cannot be updated" + }, + "ready": { + "type": "boolean", + "description": "specifies whether the container has passed its readiness probe" + }, + "restartCount": { + "type": "integer", + "format": "int32", + "description": "the number of times the container has been restarted, currently based on the number of dead containers that have not yet been removed" + }, + "state": { + "$ref": "v1beta3.ContainerState", + "description": "details about the container's current condition" + } + } + }, + "v1beta3.EndpointAddress": { + "id": "v1beta3.EndpointAddress", + "required": [ + "IP" + ], + "properties": { + "IP": { + "type": "string", + "description": "IP address of the endpoint" }, "targetRef": { "$ref": "v1beta3.ObjectReference", @@ -4111,8 +5719,51 @@ } } }, + "v1beta3.EndpointPort": { + "id": "v1beta3.EndpointPort", + "required": [ + "port" + ], + "properties": { + "name": { + "type": "string", + "description": "name of this port" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "port number of the endpoint" + }, + "protocol": { + "$ref": "v1beta3.Protocol", + "description": "protocol for this port; must be UDP or TCP; TCP if unspecified" + } + } + }, + "v1beta3.EndpointSubset": { + "id": "v1beta3.EndpointSubset", + "properties": { + "addresses": { + "type": "array", + "items": { + "$ref": "v1beta3.EndpointAddress" + }, + "description": "IP addresses which offer the related ports" + }, + "ports": { + "type": "array", + "items": { + "$ref": "v1beta3.EndpointPort" + }, + "description": "port numbers available on the related IP addresses" + } + } + }, "v1beta3.Endpoints": { "id": "v1beta3.Endpoints", + "required": [ + "subsets" + ], "properties": { "annotations": { "$ref": "v1beta3.ObjectMeta.annotations", @@ -4130,13 +5781,6 @@ "type": "string", "description": "RFC 3339 date and time at which the object will be deleted; populated by the system when a graceful deletion is requested, read-only; if not set, graceful deletion of the object has not been requested" }, - "endpoints": { - "type": "array", - "items": { - "$ref": "v1beta3.Endpoint" - }, - "description": "list of endpoints corresponding to a service" - }, "generateName": { "type": "string", "description": "an optional prefix to use to generate a unique name; has the same validation rules as name; optional, and is applied only name if is not specified" @@ -4157,10 +5801,6 @@ "type": "string", "description": "namespace of the object; cannot be updated" }, - "protocol": { - "$ref": "v1beta3.Protocol", - "description": "IP protocol for endpoint ports; must be UDP or TCP; TCP if unspecified" - }, "resourceVersion": { "type": "string", "description": "string that identifies the internal version of this object that can be used by clients to determine when objects have changed; populated by the system, read-only; value must be treated as opaque by clients and passed unmodified back to the server: https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#concurrency-control-and-consistency" @@ -4169,6 +5809,13 @@ "type": "string", "description": "URL for the object; populated by the system, read-only" }, + "subsets": { + "type": "array", + "items": { + "$ref": "v1beta3.EndpointSubset" + }, + "description": "sets of addresses and ports that comprise a service" + }, "uid": { "$ref": "types.UID", "description": "unique UUID across space and time; populated by the system; read-only" @@ -4787,10 +6434,6 @@ "v1beta3.NodeSpec": { "id": "v1beta3.NodeSpec", "properties": { - "capacity": { - "$ref": "v1beta3.ResourceList", - "description": "compute resource capacity of the node; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md" - }, "externalID": { "type": "string", "description": "external ID assigned to the node by some machine database (e.g. a cloud provider)" @@ -4815,6 +6458,10 @@ }, "description": "list of addresses reachable to the node" }, + "capacity": { + "$ref": "v1beta3.ResourceList", + "description": "compute resource capacity of the node; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md" + }, "conditions": { "type": "array", "items": { @@ -4835,12 +6482,42 @@ "id": "v1beta3.NodeSystemInfo", "required": [ "machineID", - "systemUUID" + "systemUUID", + "bootID", + "kernelVersion", + "osImage", + "containerRuntimeVersion", + "kubeletVersion", + "KubeProxyVersion" ], "properties": { + "KubeProxyVersion": { + "type": "string", + "description": "Kube-proxy version reported by the node" + }, + "bootID": { + "type": "string", + "description": "boot id is the boot-id reported by the node" + }, + "containerRuntimeVersion": { + "type": "string", + "description": "Container runtime version reported by the node through runtime remote API (e.g. docker://1.5.0)" + }, + "kernelVersion": { + "type": "string", + "description": "Kernel version reported by the node from 'uname -r' (e.g. 3.16.0-0.bpo.4-amd64)" + }, + "kubeletVersion": { + "type": "string", + "description": "Kubelet version reported by the node" + }, "machineID": { "type": "string" }, + "osImage": { + "type": "string", + "description": "OS image used reported by the node from /etc/os-release (e.g. Debian GNU/Linux 7 (wheezy))" + }, "systemUUID": { "type": "string" } @@ -5045,6 +6722,13 @@ }, "description": "current service state of pod" }, + "containerStatuses": { + "type": "array", + "items": { + "$ref": "v1beta3.ContainerStatus" + }, + "description": "list of container statuses" + }, "host": { "type": "string", "description": "host to which the pod is assigned; empty if not yet scheduled; cannot be updated" @@ -5053,10 +6737,6 @@ "type": "string", "description": "IP address of the host to which the pod is assigned; empty if not yet scheduled" }, - "info": { - "$ref": "v1beta3.PodInfo", - "description": "map of container name to container status" - }, "message": { "type": "string", "description": "human readable message indicating details about why the pod is in this condition" @@ -5596,10 +7276,38 @@ } } }, + "v1beta3.ServicePort": { + "id": "v1beta3.ServicePort", + "required": [ + "name", + "protocol", + "port", + "targetPort" + ], + "properties": { + "name": { + "type": "string", + "description": "the name of this port; optional if only one port is defined" + }, + "port": { + "type": "integer", + "format": "int32", + "description": "the port number that is exposed" + }, + "protocol": { + "$ref": "v1beta3.Protocol", + "description": "the protocol used by this port; must be UDP or TCP; TCP if unspecified" + }, + "targetPort": { + "type": "string", + "description": "the port to access on the pods targeted by the service; defaults to the service port" + } + } + }, "v1beta3.ServiceSpec": { "id": "v1beta3.ServiceSpec", "required": [ - "port", + "ports", "selector", "portalIP" ], @@ -5608,17 +7316,15 @@ "type": "boolean", "description": "set up a cloud-provider-specific load balancer on an external IP" }, - "port": { - "type": "integer", - "format": "int32", - "description": "port exposed by the service" - }, "portalIP": { "type": "string" }, - "protocol": { - "$ref": "v1beta3.Protocol", - "description": "protocol for port; must be UDP or TCP; TCP if unspecified" + "ports": { + "type": "array", + "items": { + "$ref": "v1beta3.ServicePort" + }, + "description": "ports exposed by the service" }, "publicIPs": { "type": "array", @@ -5634,10 +7340,6 @@ "sessionAffinity": { "$ref": "v1beta3.AffinityType", "description": "enable client IP based session affinity; must be ClientIP or None; defaults to None" - }, - "targetPort": { - "type": "string", - "description": "number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port" } } }, diff --git a/cluster/addons/cluster-monitoring/v1beta3/grafana-service.yaml b/cluster/addons/cluster-monitoring/v1beta3/grafana-service.yaml index bdf4080d0b1..611a8ae816a 100644 --- a/cluster/addons/cluster-monitoring/v1beta3/grafana-service.yaml +++ b/cluster/addons/cluster-monitoring/v1beta3/grafana-service.yaml @@ -6,8 +6,9 @@ metadata: kubernetes.io/cluster-service: "true" name: monitoring-grafana spec: - targetPort: 80 - port: 80 + ports: + - port: 80 + targetPort: 80 selector: name: influxGrafana kubernetes.io/cluster-service: "true" diff --git a/cluster/addons/cluster-monitoring/v1beta3/heapster-service.yaml b/cluster/addons/cluster-monitoring/v1beta3/heapster-service.yaml index e10a4f8d3fe..06919b01c46 100644 --- a/cluster/addons/cluster-monitoring/v1beta3/heapster-service.yaml +++ b/cluster/addons/cluster-monitoring/v1beta3/heapster-service.yaml @@ -6,8 +6,9 @@ metadata: kubernetes.io/cluster-service: "true" name: monitoring-heapster spec: - targetPort: 8082 - port: 80 + ports: + - port: 80 + targetPort: 8082 selector: name: heapster kubernetes.io/cluster-service: "true" diff --git a/cluster/addons/cluster-monitoring/v1beta3/influxdb-service.yaml b/cluster/addons/cluster-monitoring/v1beta3/influxdb-service.yaml index bf3465dc77f..1babd85ec38 100644 --- a/cluster/addons/cluster-monitoring/v1beta3/influxdb-service.yaml +++ b/cluster/addons/cluster-monitoring/v1beta3/influxdb-service.yaml @@ -5,8 +5,9 @@ metadata: name: influxGrafana name: monitoring-influxdb spec: - targetPort: 8086 - port: 80 + ports: + - port: 80 + targetPort: 8086 selector: name: influxGrafana diff --git a/cluster/addons/cluster-monitoring/v1beta3/influxdb-ui-service.yaml b/cluster/addons/cluster-monitoring/v1beta3/influxdb-ui-service.yaml index 5073a30817c..3c4f9d4d116 100644 --- a/cluster/addons/cluster-monitoring/v1beta3/influxdb-ui-service.yaml +++ b/cluster/addons/cluster-monitoring/v1beta3/influxdb-ui-service.yaml @@ -5,8 +5,9 @@ metadata: name: influxGrafana name: monitoring-influxdb-ui spec: - targetPort: 8083 - port: 80 + ports: + - port: 80 + targetPort: 8083 selector: name: influxGrafana diff --git a/cluster/addons/dns/kube2sky/kube2sky.go b/cluster/addons/dns/kube2sky/kube2sky.go index daf59bf76f2..e8d9aeac824 100644 --- a/cluster/addons/dns/kube2sky/kube2sky.go +++ b/cluster/addons/dns/kube2sky/kube2sky.go @@ -57,22 +57,27 @@ func addDNS(record string, service *kapi.Service, etcdClient *etcd.Client) error return nil } - svc := skymsg.Service{ - Host: service.Spec.PortalIP, - Port: service.Spec.Port, - Priority: 10, - Weight: 10, - Ttl: 30, - } - b, err := json.Marshal(svc) - if err != nil { - return err - } - // Set with no TTL, and hope that kubernetes events are accurate. + for i := range service.Spec.Ports { + svc := skymsg.Service{ + Host: service.Spec.PortalIP, + Port: service.Spec.Ports[i].Port, + Priority: 10, + Weight: 10, + Ttl: 30, + } + b, err := json.Marshal(svc) + if err != nil { + return err + } + // Set with no TTL, and hope that kubernetes events are accurate. - log.Printf("Setting dns record: %v -> %s:%d\n", record, service.Spec.PortalIP, service.Spec.Port) - _, err = etcdClient.Set(skymsg.Path(record), string(b), uint64(0)) - return err + log.Printf("Setting DNS record: %v -> %s:%d\n", record, service.Spec.PortalIP, service.Spec.Ports[i].Port) + _, err = etcdClient.Set(skymsg.Path(record), string(b), uint64(0)) + if err != nil { + return err + } + } + return nil } // Implements retry logic for arbitrary mutator. Crashes after retrying for diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index 99cca0773cc..c28c6a35eab 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -465,12 +465,14 @@ func runSelfLinkTestOnNamespace(c *client.Client, namespace string) { }, }, Spec: api.ServiceSpec{ - Port: 12345, // This is here because validation requires it. Selector: map[string]string{ "foo": "bar", }, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 12345, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } @@ -527,12 +529,14 @@ func runAtomicPutTest(c *client.Client) { }, }, Spec: api.ServiceSpec{ - Port: 12345, // This is here because validation requires it. Selector: map[string]string{ "foo": "bar", }, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 12345, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } @@ -606,12 +610,14 @@ func runPatchTest(c *client.Client) { }, }, Spec: api.ServiceSpec{ - Port: 12345, // This is here because validation requires it. Selector: map[string]string{ "foo": "bar", }, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 12345, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } @@ -747,8 +753,10 @@ func runServiceTest(client *client.Client) { Selector: map[string]string{ "name": "thisisalonglabel", }, - Port: 8080, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 8080, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } @@ -764,8 +772,10 @@ func runServiceTest(client *client.Client) { Selector: map[string]string{ "name": "thisisalonglabel", }, - Port: 8080, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 8080, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } @@ -784,8 +794,10 @@ func runServiceTest(client *client.Client) { Selector: map[string]string{ "name": "thisisalonglabel", }, - Port: 8080, - Protocol: "TCP", + Ports: []api.ServicePort{{ + Port: 8080, + Protocol: "TCP", + }}, SessionAffinity: "None", }, } diff --git a/contrib/for-tests/network-tester/service.json b/contrib/for-tests/network-tester/service.json index e220d5cad19..4ad82545007 100644 --- a/contrib/for-tests/network-tester/service.json +++ b/contrib/for-tests/network-tester/service.json @@ -8,11 +8,15 @@ } }, "spec": { - "port": 8080, - "protocol": "TCP", + "ports": [ + { + "port": 8080, + "protocol": "TCP", + "targetPort": 8080 + } + ], "selector": { "name": "nettest" - }, - "targetPort": 8080, + } } } diff --git a/examples/cassandra/v1beta3/cassandra-service.yaml b/examples/cassandra/v1beta3/cassandra-service.yaml index 08a8f744dae..1811db7753f 100644 --- a/examples/cassandra/v1beta3/cassandra-service.yaml +++ b/examples/cassandra/v1beta3/cassandra-service.yaml @@ -5,7 +5,8 @@ metadata: name: cassandra name: cassandra spec: - targetPort: 9042 - port: 9042 + ports: + - port: 9042 + targetPort: 9042 selector: name: cassandra diff --git a/examples/guestbook-go/v1beta3/guestbook-service.json b/examples/guestbook-go/v1beta3/guestbook-service.json index c7973217e81..0f515b64f76 100644 --- a/examples/guestbook-go/v1beta3/guestbook-service.json +++ b/examples/guestbook-go/v1beta3/guestbook-service.json @@ -8,9 +8,13 @@ } }, "spec":{ - "port":3000, - "containerPort":"http-server", - "protocol":"TCP", + "ports": [ + { + "port":3000, + "targetPort":"http-server", + "protocol":"TCP" + } + ], "selector":{ "name":"guestbook" } diff --git a/examples/guestbook-go/v1beta3/redis-master-service.json b/examples/guestbook-go/v1beta3/redis-master-service.json index 2296638efba..5aed7d9ff84 100644 --- a/examples/guestbook-go/v1beta3/redis-master-service.json +++ b/examples/guestbook-go/v1beta3/redis-master-service.json @@ -9,9 +9,13 @@ } }, "spec":{ - "port":6379, - "containerPort":"redis-server", - "protocol":"TCP", + "ports": [ + { + "port":6379, + "targetPort":"redis-server", + "protocol":"TCP" + } + ], "selector":{ "name":"redis", "role":"master" diff --git a/examples/guestbook-go/v1beta3/redis-slave-service.json b/examples/guestbook-go/v1beta3/redis-slave-service.json index 04b4861aa3b..2eb1fb4ad04 100644 --- a/examples/guestbook-go/v1beta3/redis-slave-service.json +++ b/examples/guestbook-go/v1beta3/redis-slave-service.json @@ -9,9 +9,13 @@ } }, "spec":{ - "port":6379, - "containerPort":"redis-server", - "protocol":"TCP", + "ports": [ + { + "port":6379, + "targetPort":"redis-server", + "protocol":"TCP" + } + ], "selector":{ "name":"redis", "role":"slave" diff --git a/examples/guestbook/v1beta3/frontend-service.json b/examples/guestbook/v1beta3/frontend-service.json index 0921ba7fcb4..07e81f9942b 100644 --- a/examples/guestbook/v1beta3/frontend-service.json +++ b/examples/guestbook/v1beta3/frontend-service.json @@ -8,9 +8,13 @@ } }, "spec":{ - "port":80, - "containerPort":80, - "protocol":"TCP", + "ports": [ + { + "port":80, + "targetPort":80, + "protocol":"TCP" + } + ], "selector":{ "name":"frontend" } diff --git a/examples/guestbook/v1beta3/redis-master-service.json b/examples/guestbook/v1beta3/redis-master-service.json index c22669b62bf..101d9ea965c 100644 --- a/examples/guestbook/v1beta3/redis-master-service.json +++ b/examples/guestbook/v1beta3/redis-master-service.json @@ -8,9 +8,13 @@ } }, "spec":{ - "port":6379, - "containerPort":6379, - "protocol":"TCP", + "ports": [ + { + "port":6379, + "targetPort":6379, + "protocol":"TCP" + } + ], "selector":{ "name":"redis-master" } diff --git a/examples/guestbook/v1beta3/redis-slave-service.json b/examples/guestbook/v1beta3/redis-slave-service.json index ad8ddf92d61..2b866b6f94a 100644 --- a/examples/guestbook/v1beta3/redis-slave-service.json +++ b/examples/guestbook/v1beta3/redis-slave-service.json @@ -8,9 +8,13 @@ } }, "spec":{ - "port":6379, - "containerPort":6379, - "protocol":"TCP", + "ports": [ + { + "port":6379, + "targetPort":6379, + "protocol":"TCP" + } + ], "selector":{ "name":"redis-slave" } diff --git a/examples/hazelcast/v1beta3/hazelcast-service.yaml b/examples/hazelcast/v1beta3/hazelcast-service.yaml index 186eaad570f..1ea5a121209 100644 --- a/examples/hazelcast/v1beta3/hazelcast-service.yaml +++ b/examples/hazelcast/v1beta3/hazelcast-service.yaml @@ -5,7 +5,8 @@ metadata: name: hazelcast name: hazelcast spec: - targetPort: 5701 - port: 5701 + ports: + - port: 5701 + targetPort: 5701 selector: name: hazelcast diff --git a/examples/mysql-wordpress-pd/v1beta3/mysql-service.yaml b/examples/mysql-wordpress-pd/v1beta3/mysql-service.yaml index 38fb46a9dee..9848475d4ab 100644 --- a/examples/mysql-wordpress-pd/v1beta3/mysql-service.yaml +++ b/examples/mysql-wordpress-pd/v1beta3/mysql-service.yaml @@ -5,8 +5,9 @@ metadata: name: mysql name: mysql spec: - targetPort: 3306 - port: 3306 + ports: + - port: 3306 + targetPort: 3306 selector: name: mysql diff --git a/examples/mysql-wordpress-pd/v1beta3/wordpress-service.yaml b/examples/mysql-wordpress-pd/v1beta3/wordpress-service.yaml index 8069b06a2bd..a2aa333faf8 100644 --- a/examples/mysql-wordpress-pd/v1beta3/wordpress-service.yaml +++ b/examples/mysql-wordpress-pd/v1beta3/wordpress-service.yaml @@ -5,8 +5,9 @@ metadata: name: wpfrontend name: wpfrontend spec: - targetPort: 80 - port: 80 + ports: + - port: 80 + targetPort: 80 selector: name: wpfrontend diff --git a/examples/redis/v1beta3/redis-sentinel-service.yaml b/examples/redis/v1beta3/redis-sentinel-service.yaml index 2c8bcaa46d7..7078d182f3f 100644 --- a/examples/redis/v1beta3/redis-sentinel-service.yaml +++ b/examples/redis/v1beta3/redis-sentinel-service.yaml @@ -6,7 +6,8 @@ metadata: role: service name: redis-sentinel spec: - targetPort: 26379 - port: 26379 + ports: + - port: 26379 + targetPort: 26379 selector: redis-sentinel: "true" diff --git a/examples/rethinkdb/v1beta3/admin-service.yaml b/examples/rethinkdb/v1beta3/admin-service.yaml index 91fc19452ac..03e00840ffb 100644 --- a/examples/rethinkdb/v1beta3/admin-service.yaml +++ b/examples/rethinkdb/v1beta3/admin-service.yaml @@ -6,8 +6,9 @@ metadata: name: rethinkdb-admin namespace: rethinkdb spec: - targetPort: 8080 - port: 8080 + ports: + - port: 8080 + targetPort: 8080 selector: db: rethinkdb role: admin diff --git a/examples/rethinkdb/v1beta3/driver-service.yaml b/examples/rethinkdb/v1beta3/driver-service.yaml index 5b5163684f4..824afac8790 100644 --- a/examples/rethinkdb/v1beta3/driver-service.yaml +++ b/examples/rethinkdb/v1beta3/driver-service.yaml @@ -6,7 +6,8 @@ metadata: name: rethinkdb-driver namespace: rethinkdb spec: - targetPort: 28015 - port: 28015 + ports: + - port: 28015 + targetPort: 28015 selector: db: rethinkdb diff --git a/examples/walkthrough/v1beta3/service.yaml b/examples/walkthrough/v1beta3/service.yaml index a6a9c187d6d..58a459e5116 100644 --- a/examples/walkthrough/v1beta3/service.yaml +++ b/examples/walkthrough/v1beta3/service.yaml @@ -3,16 +3,14 @@ kind: Service metadata: name: nginx-example spec: - # the container on each pod to connect to, can be a name - # (e.g. 'www') or a number (e.g. 80) - targetPort: 80 - # the port that this service should serve on - port: 8000 - protocol: TCP + ports: + - port: 8000 # the port that this service should serve on + # the container on each pod to connect to, can be a name + # (e.g. 'www') or a number (e.g. 80) + targetPort: 80 + protocol: TCP # just like the selector in the replication controller, # but this time it identifies the set of pods to load balance # traffic to. selector: name: nginx - - diff --git a/hack/lib/test.sh b/hack/lib/test.sh index dd1c2099237..bc8008c033d 100644 --- a/hack/lib/test.sh +++ b/hack/lib/test.sh @@ -31,7 +31,7 @@ kube::test::get_object_assert() { local request=$2 local expected=$3 - res=$(eval kubectl get "${kube_flags[@]}" $object -o template -t "$request") + res=$(eval kubectl get "${kube_flags[@]}" $object -o template -t \"$request\") if [[ "$res" =~ ^$expected$ ]]; then echo -n ${green} diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 08b235f5d12..75088618f29 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -129,17 +129,17 @@ for version in "${kube_api_versions[@]}"; do ) [ "$(kubectl get minions -t $'{{ .apiVersion }}' "${kube_flags[@]}")" == "${version}" ] fi - id_field="id" - labels_field="labels" - service_selector_field="selector" - rc_replicas_field="desiredState.replicas" - port_field="port" + id_field=".id" + labels_field=".labels" + service_selector_field=".selector" + rc_replicas_field=".desiredState.replicas" + port_field=".port" if [ "$version" = "v1beta3" ]; then - id_field="metadata.name" - labels_field="metadata.labels" - service_selector_field="spec.selector" - rc_replicas_field="spec.replicas" - port_field="spec.port" + id_field=".metadata.name" + labels_field=".metadata.labels" + service_selector_field=".spec.selector" + rc_replicas_field=".spec.replicas" + port_field="(index .spec.ports 0).port" fi # Passing no arguments to create is an error @@ -153,14 +153,14 @@ for version in "${kube_api_versions[@]}"; do ### Create POD valid-pod from JSON # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create "${kube_flags[@]}" -f examples/limitrange/valid-pod.json # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' - kube::test::get_object_assert 'pod valid-pod' "{{.$id_field}}" 'valid-pod' - kube::test::get_object_assert 'pod/valid-pod' "{{.$id_field}}" 'valid-pod' - kube::test::get_object_assert 'pods/valid-pod' "{{.$id_field}}" 'valid-pod' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert 'pod valid-pod' "{{$id_field}}" 'valid-pod' + kube::test::get_object_assert 'pod/valid-pod' "{{$id_field}}" 'valid-pod' + kube::test::get_object_assert 'pods/valid-pod' "{{$id_field}}" 'valid-pod' # Describe command should print detailed information kube::test::describe_object_assert pods 'valid-pod' "Name:" "Image(s):" "Host:" "Labels:" "Status:" "Replication Controllers" @@ -169,165 +169,165 @@ for version in "${kube_api_versions[@]}"; do ### Delete POD valid-pod by id # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete pod valid-pod "${kube_flags[@]}" # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ### Create POD valid-pod from dumped YAML # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command echo "${output_pod}" | kubectl create -f - "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete POD valid-pod from JSON # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete -f examples/limitrange/valid-pod.json "${kube_flags[@]}" # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ### Create POD redis-master from JSON # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete POD valid-pod with label # Pre-condition: valid-pod POD is running - kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{.$id_field}}:{{end}}' 'valid-pod:' + kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{$id_field}}:{{end}}' 'valid-pod:' # Command kubectl delete pods -l'name in (valid-pod)' "${kube_flags[@]}" # Post-condition: no POD is running - kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{.$id_field}}:{{end}}' '' + kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{$id_field}}:{{end}}' '' ### Create POD valid-pod from JSON # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete PODs with no parameter mustn't kill everything # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command ! kubectl delete pods "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete PODs with --all and a label selector is not permitted # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command ! kubectl delete --all pods -l'name in (valid-pod)' "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete all PODs # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete --all pods "${kube_flags[@]}" # --all remove all the pods # Post-condition: no POD is running - kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{.$id_field}}:{{end}}' '' + kube::test::get_object_assert "pods -l'name in (valid-pod)'" '{{range.items}}{{$id_field}}:{{end}}' '' ### Create two PODs # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" kubectl create -f examples/redis/redis-proxy.yaml "${kube_flags[@]}" # Post-condition: valid-pod and redis-proxy PODs are running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'redis-proxy:valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'redis-proxy:valid-pod:' ### Delete multiple PODs at once # Pre-condition: valid-pod and redis-proxy PODs are running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'redis-proxy:valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'redis-proxy:valid-pod:' # Command kubectl delete pods valid-pod redis-proxy "${kube_flags[@]}" # delete multiple pods at once # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ### Create two PODs # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" kubectl create -f examples/redis/redis-proxy.yaml "${kube_flags[@]}" # Post-condition: valid-pod and redis-proxy PODs are running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'redis-proxy:valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'redis-proxy:valid-pod:' ### Stop multiple PODs at once # Pre-condition: valid-pod and redis-proxy PODs are running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'redis-proxy:valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'redis-proxy:valid-pod:' # Command kubectl stop pods valid-pod redis-proxy "${kube_flags[@]}" # stop multiple pods at once # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ### Create valid-pod POD # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Label the valid-pod POD # Pre-condition: valid-pod is not labelled - kube::test::get_object_assert 'pod valid-pod' "{{range.$labels_field}}{{.}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert 'pod valid-pod' "{{range$labels_field}}{{.}}:{{end}}" 'valid-pod:' # Command kubectl label pods valid-pod new-name=new-valid-pod "${kube_flags[@]}" # Post-conditon: valid-pod is labelled - kube::test::get_object_assert 'pod valid-pod' "{{range.$labels_field}}{{.}}:{{end}}" 'valid-pod:new-valid-pod:' + kube::test::get_object_assert 'pod valid-pod' "{{range$labels_field}}{{.}}:{{end}}" 'valid-pod:new-valid-pod:' ### Delete POD by label # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete pods -lnew-name=new-valid-pod "${kube_flags[@]}" # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ### Create valid-pod POD # Pre-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" # Post-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Overwriting an existing label is not permitted # Pre-condition: name is valid-pod - kube::test::get_object_assert 'pod valid-pod' "{{.${labels_field}.name}}" 'valid-pod' + kube::test::get_object_assert 'pod valid-pod' "{{${labels_field}.name}}" 'valid-pod' # Command ! kubectl label pods valid-pod name=valid-pod-super-sayan "${kube_flags[@]}" # Post-condition: name is still valid-pod - kube::test::get_object_assert 'pod valid-pod' "{{.${labels_field}.name}}" 'valid-pod' + kube::test::get_object_assert 'pod valid-pod' "{{${labels_field}.name}}" 'valid-pod' ### --overwrite must be used to overwrite existing label, can be applied to all resources # Pre-condition: name is valid-pod - kube::test::get_object_assert 'pod valid-pod' "{{.${labels_field}.name}}" 'valid-pod' + kube::test::get_object_assert 'pod valid-pod' "{{${labels_field}.name}}" 'valid-pod' # Command kubectl label --overwrite pods --all name=valid-pod-super-sayan "${kube_flags[@]}" # Post-condition: name is valid-pod-super-sayan - kube::test::get_object_assert 'pod valid-pod' "{{.${labels_field}.name}}" 'valid-pod-super-sayan' + kube::test::get_object_assert 'pod valid-pod' "{{${labels_field}.name}}" 'valid-pod-super-sayan' ### Delete POD by label # Pre-condition: valid-pod POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete pods -l'name in (valid-pod-super-sayan)' "${kube_flags[@]}" # Post-condition: no POD is running - kube::test::get_object_assert pods "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert pods "{{range.items}}{{$id_field}}:{{end}}" '' ############## @@ -336,19 +336,19 @@ for version in "${kube_api_versions[@]}"; do ### Create POD valid-pod in specific namespace # Pre-condition: no POD is running - kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create "${kube_flags[@]}" --namespace=other -f examples/limitrange/valid-pod.json # Post-condition: valid-pod POD is running - kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' ### Delete POD valid-pod in specific namespace # Pre-condition: valid-pod POD is running - kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{.$id_field}}:{{end}}" 'valid-pod:' + kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" 'valid-pod:' # Command kubectl delete "${kube_flags[@]}" pod --namespace=other valid-pod # Post-condition: no POD is running - kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" '' ############ @@ -359,11 +359,11 @@ for version in "${kube_api_versions[@]}"; do ### Create redis-master service from JSON # Pre-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' # Command kubectl create -f examples/guestbook/redis-master-service.json "${kube_flags[@]}" # Post-condition: redis-master service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' # Describe command should print detailed information kube::test::describe_object_assert services 'redis-master' "Name:" "Labels:" "Selector:" "IP:" "Port:" "Endpoints:" "Session Affinity:" @@ -372,23 +372,23 @@ for version in "${kube_api_versions[@]}"; do ### Delete redis-master-service by id # Pre-condition: redis-master service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' # Command kubectl delete service redis-master "${kube_flags[@]}" # Post-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' ### Create redis-master-service from dumped JSON # Pre-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' # Command echo "${output_service}" | kubectl create -f - "${kube_flags[@]}" # Post-condition: redis-master service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' ### Create redis-master-${version}-test service # Pre-condition: redis-master-service service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:' # Command kubectl create -f - "${kube_flags[@]}" << __EOF__ { @@ -400,43 +400,43 @@ for version in "${kube_api_versions[@]}"; do } __EOF__ # Post-condition:redis-master-service service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:service-.*-test:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:service-.*-test:' # Command kubectl update service "${kube_flags[@]}" service-${version}-test --patch="{\"selector\":{\"my\":\"test-label\"},\"apiVersion\":\"v1beta1\"}" # Post-condition: selector.version == ${version} # This test works only in v1beta1 and v1beta2 # https://github.com/GoogleCloudPlatform/kubernetes/issues/4771 - kube::test::get_object_assert "service service-${version}-test" "{{range.$service_selector_field}}{{.}}{{end}}" "test-label" + kube::test::get_object_assert "service service-${version}-test" "{{range$service_selector_field}}{{.}}{{end}}" "test-label" ### Identity kubectl get service "${kube_flags[@]}" service-${version}-test -o json | kubectl update "${kube_flags[@]}" -f - ### Delete services by id # Pre-condition: redis-master-service service is running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:service-.*-test:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:service-.*-test:' # Command kubectl delete service redis-master "${kube_flags[@]}" kubectl delete service "service-${version}-test" "${kube_flags[@]}" # Post-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' ### Create two services # Pre-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' # Command kubectl create -f examples/guestbook/redis-master-service.json "${kube_flags[@]}" kubectl create -f examples/guestbook/redis-slave-service.json "${kube_flags[@]}" # Post-condition: redis-master and redis-slave services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:redis-slave:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:redis-slave:' ### Delete multiple services at once # Pre-condition: redis-master and redis-slave services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:redis-slave:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:redis-master:redis-slave:' # Command kubectl delete services redis-master redis-slave "${kube_flags[@]}" # delete multiple services at once # Post-condition: Only the default kubernetes services are running - kube::test::get_object_assert services "{{range.items}}{{.$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' + kube::test::get_object_assert services "{{range.items}}{{$id_field}}:{{end}}" 'kubernetes:kubernetes-ro:' ########################### @@ -447,82 +447,82 @@ __EOF__ ### Create replication controller frontend from JSON # Pre-condition: no replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/guestbook/frontend-controller.json "${kube_flags[@]}" # Post-condition: frontend replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" 'frontend-controller:' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" 'frontend-controller:' # Describe command should print detailed information kube::test::describe_object_assert rc 'frontend-controller' "Name:" "Image(s):" "Labels:" "Selector:" "Replicas:" "Pods Status:" ### Resize replication controller frontend with current-replicas and replicas # Pre-condition: 3 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '3' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '3' # Command kubectl resize --current-replicas=3 --replicas=2 replicationcontrollers frontend-controller "${kube_flags[@]}" # Post-condition: 2 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '2' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '2' ### Resize replication controller frontend with (wrong) current-replicas and replicas # Pre-condition: 2 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '2' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '2' # Command ! kubectl resize --current-replicas=3 --replicas=2 replicationcontrollers frontend-controller "${kube_flags[@]}" # Post-condition: nothing changed - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '2' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '2' ### Resize replication controller frontend with replicas only # Pre-condition: 2 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '2' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '2' # Command kubectl resize --replicas=3 replicationcontrollers frontend-controller "${kube_flags[@]}" # Post-condition: 3 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '3' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '3' ### Expose replication controller as service # Pre-condition: 3 replicas - kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '3' + kube::test::get_object_assert 'rc frontend-controller' "{{$rc_replicas_field}}" '3' # Command kubectl expose rc frontend-controller --port=80 "${kube_flags[@]}" # Post-condition: service exists - kube::test::get_object_assert 'service frontend-controller' "{{.$port_field}}" '80' + kube::test::get_object_assert 'service frontend-controller' "{{$port_field}}" '80' # Command kubectl expose service frontend-controller --port=443 --service-name=frontend-controller-2 "${kube_flags[@]}" # Post-condition: service exists - kube::test::get_object_assert 'service frontend-controller-2' "{{.$port_field}}" '443' + kube::test::get_object_assert 'service frontend-controller-2' "{{$port_field}}" '443' # Command kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" kubectl expose pod valid-pod --port=444 --service-name=frontend-controller-3 "${kube_flags[@]}" # Post-condition: service exists - kube::test::get_object_assert 'service frontend-controller-3' "{{.$port_field}}" '444' + kube::test::get_object_assert 'service frontend-controller-3' "{{$port_field}}" '444' # Cleanup services kubectl delete pod valid-pod "${kube_flags[@]}" kubectl delete service frontend-controller{,-2,-3} "${kube_flags[@]}" ### Delete replication controller with id # Pre-condition: frontend replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" 'frontend-controller:' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" 'frontend-controller:' # Command kubectl delete rc frontend-controller "${kube_flags[@]}" # Post-condition: no replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' ### Create two replication controllers # Pre-condition: no replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' # Command kubectl create -f examples/guestbook/frontend-controller.json "${kube_flags[@]}" kubectl create -f examples/guestbook/redis-slave-controller.json "${kube_flags[@]}" # Post-condition: frontend and redis-slave - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" 'frontend-controller:redis-slave-controller:' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" 'frontend-controller:redis-slave-controller:' ### Delete multiple controllers at once # Pre-condition: frontend and redis-slave - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" 'frontend-controller:redis-slave-controller:' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" 'frontend-controller:redis-slave-controller:' # Command kubectl delete rc frontend-controller redis-slave-controller "${kube_flags[@]}" # delete multiple controllers at once # Post-condition: no replication controller is running - kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" '' + kube::test::get_object_assert rc "{{range.items}}{{$id_field}}:{{end}}" '' ######### @@ -531,7 +531,7 @@ __EOF__ kube::log::status "Testing kubectl(${version}:nodes)" - kube::test::get_object_assert nodes "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:' + kube::test::get_object_assert nodes "{{range.items}}{{$id_field}}:{{end}}" '127.0.0.1:' kube::test::describe_object_assert nodes "127.0.0.1" "Name:" "Labels:" "CreationTimestamp:" "Conditions:" "Addresses:" "Capacity:" "Pods:" @@ -543,7 +543,7 @@ __EOF__ if [[ "${version}" != "v1beta3" ]]; then kube::log::status "Testing kubectl(${version}:minions)" - kube::test::get_object_assert minions "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:' + kube::test::get_object_assert minions "{{range.items}}{{$id_field}}:{{end}}" '127.0.0.1:' # TODO: I should be a MinionList instead of List kube::test::get_object_assert minions '{{.kind}}' 'List' @@ -557,7 +557,7 @@ __EOF__ ##################### kube::log::status "Testing kubectl(${version}:multiget)" - kube::test::get_object_assert 'nodes/127.0.0.1 service/kubernetes' "{{range.items}}{{.$id_field}}:{{end}}" '127.0.0.1:kubernetes:' + kube::test::get_object_assert 'nodes/127.0.0.1 service/kubernetes' "{{range.items}}{{$id_field}}:{{end}}" '127.0.0.1:kubernetes:' ########### diff --git a/pkg/api/endpoints/util.go b/pkg/api/endpoints/util.go index 6ca43a78d01..6bb647a69ce 100644 --- a/pkg/api/endpoints/util.go +++ b/pkg/api/endpoints/util.go @@ -97,12 +97,27 @@ func (set addressSet) Insert(addr *api.EndpointAddress) { func hashAddresses(addrs addressSet) string { // Flatten the list of addresses into a string so it can be used as a - // map key. + // map key. Unfortunately, DeepHashObject is implemented in terms of + // spew, and spew does not handle non-primitive map keys well. So + // first we collapse it into a slice, sort the slice, then hash that. + slice := []*api.EndpointAddress{} + for k := range addrs { + slice = append(slice, k) + } + sort.Sort(addrPtrsByIP(slice)) hasher := md5.New() - util.DeepHashObject(hasher, addrs) + util.DeepHashObject(hasher, slice) return hex.EncodeToString(hasher.Sum(nil)[0:]) } +type addrPtrsByIP []*api.EndpointAddress + +func (sl addrPtrsByIP) Len() int { return len(sl) } +func (sl addrPtrsByIP) Swap(i, j int) { sl[i], sl[j] = sl[j], sl[i] } +func (sl addrPtrsByIP) Less(i, j int) bool { + return bytes.Compare([]byte(sl[i].IP), []byte(sl[j].IP)) < 0 +} + // SortSubsets sorts an array of EndpointSubset objects in place. For ease of // use it returns the input slice. func SortSubsets(subsets []api.EndpointSubset) []api.EndpointSubset { diff --git a/pkg/api/rest/update_test.go b/pkg/api/rest/update_test.go index 5c6eaa656ba..79266183051 100644 --- a/pkg/api/rest/update_test.go +++ b/pkg/api/rest/update_test.go @@ -23,96 +23,86 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) +func makeValidService() api.Service { + return api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "valid", + Namespace: "default", + Labels: map[string]string{}, + Annotations: map[string]string{}, + ResourceVersion: "1", + }, + Spec: api.ServiceSpec{ + Selector: map[string]string{"key": "val"}, + SessionAffinity: "None", + Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}}, + }, + } +} + +// TODO: This should be done on types that are not part of our API func TestBeforeUpdate(t *testing.T) { - tests := []struct { - old runtime.Object - obj runtime.Object + testCases := []struct { + name string + tweakSvc func(oldSvc, newSvc *api.Service) // given basic valid services, each test case can customize them expectErr bool }{ { - obj: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: "#$%%invalid", - }, + name: "no change", + tweakSvc: func(oldSvc, newSvc *api.Service) { + // nothing }, - old: &api.Service{}, - expectErr: true, + expectErr: false, }, { - obj: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: "valid", - }, + name: "change port", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.Ports[0].Port++ }, - old: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "bar", - ResourceVersion: "1", - Namespace: "valid", - }, + expectErr: false, + }, + { + name: "bad namespace", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Namespace = "#$%%invalid" }, expectErr: true, }, { - obj: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: "valid", - }, - Spec: api.ServiceSpec{ - PortalIP: "1.2.3.4", - }, - }, - old: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: "valid", - }, - Spec: api.ServiceSpec{ - PortalIP: "4.3.2.1", - }, + name: "change name", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Name += "2" }, expectErr: true, }, { - obj: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: api.NamespaceDefault, - }, - Spec: api.ServiceSpec{ - PortalIP: "1.2.3.4", - Selector: map[string]string{"foo": "bar"}, - }, + name: "change portal IP", + tweakSvc: func(oldSvc, newSvc *api.Service) { + oldSvc.Spec.PortalIP = "1.2.3.4" + newSvc.Spec.PortalIP = "4.3.2.1" }, - old: &api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - ResourceVersion: "1", - Namespace: api.NamespaceDefault, - }, - Spec: api.ServiceSpec{ - PortalIP: "1.2.3.4", - Selector: map[string]string{"bar": "foo"}, - }, + expectErr: true, + }, + { + name: "change selectpor", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.Selector = map[string]string{"newkey": "newvalue"} }, + expectErr: false, }, } - for _, test := range tests { + + for _, tc := range testCases { + oldSvc := makeValidService() + newSvc := makeValidService() + tc.tweakSvc(&oldSvc, &newSvc) ctx := api.NewDefaultContext() - err := BeforeUpdate(Services, ctx, test.obj, test.old) - if test.expectErr && err == nil { - t.Errorf("unexpected non-error for %v", test) + err := BeforeUpdate(Services, ctx, runtime.Object(&oldSvc), runtime.Object(&newSvc)) + if tc.expectErr && err == nil { + t.Errorf("unexpected non-error for %q", tc.name) } - if !test.expectErr && err != nil { - t.Errorf("unexpected error: %v for %v -> %v", err, test.obj, test.old) + if !tc.expectErr && err != nil { + t.Errorf("unexpected error for %q: %v", tc.name, err) } } } diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 85a2d27eaff..7c7870d22f3 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -216,11 +216,18 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { }, func(ss *api.ServiceSpec, c fuzz.Continue) { c.FuzzNoCustom(ss) // fuzz self without calling this function again - switch ss.TargetPort.Kind { - case util.IntstrInt: - ss.TargetPort.IntVal = 1 + ss.TargetPort.IntVal%65535 // non-zero - case util.IntstrString: - ss.TargetPort.StrVal = "x" + ss.TargetPort.StrVal // non-empty + if len(ss.Ports) == 0 { + // There must be at least 1 port. + ss.Ports = append(ss.Ports, api.ServicePort{}) + c.Fuzz(&ss.Ports[0]) + } + for i := range ss.Ports { + switch ss.Ports[i].TargetPort.Kind { + case util.IntstrInt: + ss.Ports[i].TargetPort.IntVal = 1 + ss.Ports[i].TargetPort.IntVal%65535 // non-zero + case util.IntstrString: + ss.Ports[i].TargetPort.StrVal = "x" + ss.Ports[i].TargetPort.StrVal // non-empty + } } }, ) diff --git a/pkg/api/types.go b/pkg/api/types.go index 29582aea88b..de435151603 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -865,12 +865,8 @@ type ServiceStatus struct{} // ServiceSpec describes the attributes that a user creates on a service type ServiceSpec struct { - // Port is the TCP or UDP port that will be made available to each pod for connecting to the pods - // proxied by this service. - Port int `json:"port"` - - // Required: Supports "TCP" and "UDP". - Protocol Protocol `json:"protocol,omitempty"` + // Required: The list of ports that are exposed by this service. + Ports []ServicePort `json:"ports"` // This service will route traffic to pods having labels matching this selector. If empty or not present, // the service is assumed to have endpoints set by an external process and Kubernetes will not modify @@ -893,17 +889,32 @@ type ServiceSpec struct { // For hostnames, the user will use a CNAME record (instead of using an A record with the IP) PublicIPs []string `json:"publicIPs,omitempty"` - // TargetPort is the name or number of the port on the container to direct traffic to. - // This is useful if the containers the service points to have multiple open ports. - // Optional: If unspecified, the first port on the container will be used. - // As of v1beta3 this field will become required in the internal API, - // and the versioned APIs must provide a default value. - TargetPort util.IntOrString `json:"targetPort,omitempty"` - // Required: Supports "ClientIP" and "None". Used to maintain session affinity. SessionAffinity AffinityType `json:"sessionAffinity,omitempty"` } +type ServicePort struct { + // Optional if only one ServicePort is defined on this service: The + // name of this port within the service. This must be a DNS_LABEL. + // All ports within a ServiceSpec must have unique names. This maps to + // the 'Name' field in EndpointPort objects. + Name string `json:"name"` + + // The IP protocol for this port. Supports "TCP" and "UDP". + Protocol Protocol `json:"protocol"` + + // The port that will be exposed on the service. + Port int `json:"port"` + + // Optional: The target port on pods selected by this service. If this + // is a string, it will be looked up as a named port in the target + // Pod's container ports. If this is not specified, the first port on + // the destination pod will be used. This behavior is deprecated. As + // of v1beta3 the default value is the sames as the Port field (an + // identity map). + TargetPort util.IntOrString `json:"targetPort"` +} + // Service is a named abstraction of software service (for example, mysql) consisting of local port // (for example 3306) that the proxy listens on, and the selector that determines which pods // will answer requests sent through the proxy. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 64bbc61c967..43a504b256f 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -709,14 +709,28 @@ func init() { return err } - out.Port = in.Spec.Port - out.Protocol = Protocol(in.Spec.Protocol) + // Produce legacy fields. + if len(in.Spec.Ports) > 0 { + out.PortName = in.Spec.Ports[0].Name + out.Port = in.Spec.Ports[0].Port + out.Protocol = Protocol(in.Spec.Ports[0].Protocol) + out.ContainerPort = in.Spec.Ports[0].TargetPort + } + // Copy modern fields. + for i := range in.Spec.Ports { + out.Ports = append(out.Ports, ServicePort{ + Name: in.Spec.Ports[i].Name, + Port: in.Spec.Ports[i].Port, + Protocol: Protocol(in.Spec.Ports[i].Protocol), + ContainerPort: in.Spec.Ports[i].TargetPort, + }) + } + if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { return err } out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.PublicIPs = in.Spec.PublicIPs - out.ContainerPort = in.Spec.TargetPort out.PortalIP = in.Spec.PortalIP if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { return err @@ -735,14 +749,31 @@ func init() { return err } - out.Spec.Port = in.Port - out.Spec.Protocol = newer.Protocol(in.Protocol) + if len(in.Ports) == 0 && in.Port != 0 { + // Use legacy fields to produce modern fields. + out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{ + Name: in.PortName, + Port: in.Port, + Protocol: newer.Protocol(in.Protocol), + TargetPort: in.ContainerPort, + }) + } else { + // Use modern fields, ignore legacy. + for i := range in.Ports { + out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{ + Name: in.Ports[i].Name, + Port: in.Ports[i].Port, + Protocol: newer.Protocol(in.Ports[i].Protocol), + TargetPort: in.Ports[i].ContainerPort, + }) + } + } + if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { return err } out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.PublicIPs = in.PublicIPs - out.Spec.TargetPort = in.ContainerPort out.Spec.PortalIP = in.PortalIP if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { return err diff --git a/pkg/api/v1beta1/conversion_test.go b/pkg/api/v1beta1/conversion_test.go index 548d7b365be..f8953895207 100644 --- a/pkg/api/v1beta1/conversion_test.go +++ b/pkg/api/v1beta1/conversion_test.go @@ -284,6 +284,191 @@ func TestServiceEmptySelector(t *testing.T) { } } +func TestServicePorts(t *testing.T) { + testCases := []struct { + given current.Service + expected newer.Service + roundtrip current.Service + }{ + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "legacy-with-defaults", + }, + Port: 111, + Protocol: current.ProtocolTCP, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Port: 111, + Protocol: newer.ProtocolTCP, + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Port: 111, + Protocol: current.ProtocolTCP, + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "legacy-full", + }, + PortName: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolTCP, + TargetPort: util.NewIntOrStringFromString("p"), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "both", + }, + PortName: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + Ports: []current.ServicePort{{ + Name: "q", + Port: 222, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "q", + Port: 222, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "q", + Port: 222, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "one", + }, + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "two", + }, + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromInt(76), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: newer.ProtocolTCP, + TargetPort: util.NewIntOrStringFromInt(76), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromInt(76), + }}, + }, + }, + } + + for i, tc := range testCases { + // Convert versioned -> internal. + got := newer.Service{} + if err := Convert(&tc.given, &got); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if !reflect.DeepEqual(got.Spec.Ports, tc.expected.Spec.Ports) { + t.Errorf("[Case: %d] Expected %v, got %v", i, tc.expected.Spec.Ports, got.Spec.Ports) + } + + // Convert internal -> versioned. + got2 := current.Service{} + if err := Convert(&got, &got2); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if !reflect.DeepEqual(got2.Ports, tc.roundtrip.Ports) { + t.Errorf("[Case: %d] Expected %v, got %v", i, tc.roundtrip.Ports, got2.Ports) + } + } +} + func TestPullPolicyConversion(t *testing.T) { table := []struct { versioned current.PullPolicy @@ -527,7 +712,7 @@ func TestEndpointsConversion(t *testing.T) { continue } if got2.Protocol != tc.given.Protocol || !newer.Semantic.DeepEqual(got2.Endpoints, tc.given.Endpoints) { - t.Errorf("[Case: %d] Expected %#v, got %#v", i, tc.given.Endpoints, got2.Endpoints) + t.Errorf("[Case: %d] Expected %s %#v, got %s %#v", i, tc.given.Protocol, tc.given.Endpoints, got2.Protocol, got2.Endpoints) } } } diff --git a/pkg/api/v1beta1/defaults.go b/pkg/api/v1beta1/defaults.go index 629668d4e78..6011272a045 100644 --- a/pkg/api/v1beta1/defaults.go +++ b/pkg/api/v1beta1/defaults.go @@ -67,6 +67,15 @@ func init() { if obj.SessionAffinity == "" { obj.SessionAffinity = AffinityTypeNone } + for i := range obj.Ports { + sp := &obj.Ports[i] + if sp.Protocol == "" { + sp.Protocol = ProtocolTCP + } + if sp.ContainerPort == util.NewIntOrStringFromInt(0) || sp.ContainerPort == util.NewIntOrStringFromString("") { + sp.ContainerPort = util.NewIntOrStringFromInt(sp.Port) + } + } }, func(obj *PodSpec) { if obj.DNSPolicy == "" { diff --git a/pkg/api/v1beta1/defaults_test.go b/pkg/api/v1beta1/defaults_test.go index 680b5d6b210..19c27c5067b 100644 --- a/pkg/api/v1beta1/defaults_test.go +++ b/pkg/api/v1beta1/defaults_test.go @@ -23,6 +23,7 @@ import ( newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { @@ -154,3 +155,35 @@ func TestSetDefaultContainerManifestHostNetwork(t *testing.T) { t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum) } } + +func TestSetDefaultServicePort(t *testing.T) { + // Unchanged if set. + in := ¤t.Service{Ports: []current.ServicePort{{Protocol: "UDP", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(118)}}} + out := roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolUDP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != in.Ports[0].ContainerPort { + t.Errorf("Expected port %d, got %d", in.Ports[0].ContainerPort, out.Ports[0].ContainerPort) + } + + // Defaulted. + in = ¤t.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(0)}}} + out = roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) { + t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort) + } + + // Defaulted. + in = ¤t.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromString("")}}} + out = roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) { + t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort) + } +} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index d3954fa1c97..4559bbb3661 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -720,14 +720,21 @@ type Service struct { // Required. Port int `json:"port" description:"port exposed by the service"` + // Optional: The name of the first port. + PortName string `json:"portName,omitempty" description:"the name of the first port; optional"` // Optional: Defaults to "TCP". Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` + // ContainerPort is the name or number of the port on the container to direct traffic to. + // This is useful if the containers the service points to have multiple open ports. + // Optional: If unspecified, the first port on the container will be used. + ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` // This service's labels. Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"` // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` + // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` @@ -735,11 +742,6 @@ type Service struct { // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` - // ContainerPort is the name or number of the port on the container to direct traffic to. - // This is useful if the containers the service points to have multiple open ports. - // Optional: If unspecified, the first port on the container will be used. - ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` - // PortalIP is usually assigned by the master. If specified by the user // we will try to respect it or else fail the request. This field can // not be changed by updates. @@ -752,6 +754,35 @@ type Service struct { // Optional: Supports "ClientIP" and "None". Used to maintain session affinity. SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` + + // Optional: Ports to expose on the service. If this field is + // specified, the legacy fields (Port, PortName, Protocol, and + // ContainerPort) will be overwritten by the first member of this + // array. If this field is not specified, it will be populated from + // the legacy fields. + Ports []ServicePort `json:"ports" description:"ports to be exposed on the service; if this field is specified, the legacy fields (Port, PortName, Protocol, and ContainerPort) will be overwritten by the first member of this array; if this field is not specified, it will be populated from the legacy fields"` +} + +type ServicePort struct { + // Required: The name of this port within the service. This must be a + // DNS_LABEL. All ports within a ServiceSpec must have unique names. + // This maps to the 'Name' field in EndpointPort objects. + Name string `json:"name" description:"the name of this port; optional if only one port is defined"` + + // Optional: The IP protocol for this port. Supports "TCP" and "UDP", + // default is TCP. + Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"` + + // Required: The port that will be exposed. + Port int `json:"port" description:"the port number that is exposed"` + + // Optional: The port number on the target pod to direct traffic to. + // This is useful if the containers the service points to have multiple + // open ports. If this is a string, it will be looked up as a named + // port in the target Pod's container ports. If unspecified, the value + // of Port is used (an identity map) - note this is a different default + // than Service.ContainerPort. + ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` } // EndpointObjectReference is a reference to an object exposing the endpoint diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 65a3a2d8921..731a5fb9709 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -640,14 +640,28 @@ func init() { return err } - out.Port = in.Spec.Port - out.Protocol = Protocol(in.Spec.Protocol) + // Produce legacy fields. + if len(in.Spec.Ports) > 0 { + out.PortName = in.Spec.Ports[0].Name + out.Port = in.Spec.Ports[0].Port + out.Protocol = Protocol(in.Spec.Ports[0].Protocol) + out.ContainerPort = in.Spec.Ports[0].TargetPort + } + // Copy modern fields. + for i := range in.Spec.Ports { + out.Ports = append(out.Ports, ServicePort{ + Name: in.Spec.Ports[i].Name, + Port: in.Spec.Ports[i].Port, + Protocol: Protocol(in.Spec.Ports[i].Protocol), + ContainerPort: in.Spec.Ports[i].TargetPort, + }) + } + if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { return err } out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.PublicIPs = in.Spec.PublicIPs - out.ContainerPort = in.Spec.TargetPort out.PortalIP = in.Spec.PortalIP if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { return err @@ -666,14 +680,31 @@ func init() { return err } - out.Spec.Port = in.Port - out.Spec.Protocol = newer.Protocol(in.Protocol) + if len(in.Ports) == 0 && in.Port != 0 { + // Use legacy fields to produce modern fields. + out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{ + Name: in.PortName, + Port: in.Port, + Protocol: newer.Protocol(in.Protocol), + TargetPort: in.ContainerPort, + }) + } else { + // Use modern fields, ignore legacy. + for i := range in.Ports { + out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{ + Name: in.Ports[i].Name, + Port: in.Ports[i].Port, + Protocol: newer.Protocol(in.Ports[i].Protocol), + TargetPort: in.Ports[i].ContainerPort, + }) + } + } + if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { return err } out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.PublicIPs = in.PublicIPs - out.Spec.TargetPort = in.ContainerPort out.Spec.PortalIP = in.PortalIP if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { return err diff --git a/pkg/api/v1beta2/conversion_test.go b/pkg/api/v1beta2/conversion_test.go index 98228441b04..a30a955dcd5 100644 --- a/pkg/api/v1beta2/conversion_test.go +++ b/pkg/api/v1beta2/conversion_test.go @@ -18,6 +18,7 @@ package v1beta2_test import ( "encoding/json" + "reflect" "testing" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -58,6 +59,191 @@ func TestServiceEmptySelector(t *testing.T) { } } +func TestServicePorts(t *testing.T) { + testCases := []struct { + given current.Service + expected newer.Service + roundtrip current.Service + }{ + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "legacy-with-defaults", + }, + Port: 111, + Protocol: current.ProtocolTCP, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Port: 111, + Protocol: newer.ProtocolTCP, + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Port: 111, + Protocol: current.ProtocolTCP, + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "legacy-full", + }, + PortName: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolTCP, + TargetPort: util.NewIntOrStringFromString("p"), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "both", + }, + PortName: "p", + Port: 111, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromString("p"), + Ports: []current.ServicePort{{ + Name: "q", + Port: 222, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "q", + Port: 222, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "q", + Port: 222, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "one", + }, + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }}, + }, + }, + { + given: current.Service{ + TypeMeta: current.TypeMeta{ + ID: "two", + }, + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromInt(76), + }}, + }, + expected: newer.Service{ + Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{ + Name: "p", + Port: 111, + Protocol: newer.ProtocolUDP, + TargetPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: newer.ProtocolTCP, + TargetPort: util.NewIntOrStringFromInt(76), + }}}, + }, + roundtrip: current.Service{ + Ports: []current.ServicePort{{ + Name: "p", + Port: 111, + Protocol: current.ProtocolUDP, + ContainerPort: util.NewIntOrStringFromInt(93), + }, { + Name: "q", + Port: 222, + Protocol: current.ProtocolTCP, + ContainerPort: util.NewIntOrStringFromInt(76), + }}, + }, + }, + } + + for i, tc := range testCases { + // Convert versioned -> internal. + got := newer.Service{} + if err := newer.Scheme.Convert(&tc.given, &got); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if !reflect.DeepEqual(got.Spec.Ports, tc.expected.Spec.Ports) { + t.Errorf("[Case: %d] Expected %v, got %v", i, tc.expected.Spec.Ports, got.Spec.Ports) + } + + // Convert internal -> versioned. + got2 := current.Service{} + if err := newer.Scheme.Convert(&got, &got2); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if !reflect.DeepEqual(got2.Ports, tc.roundtrip.Ports) { + t.Errorf("[Case: %d] Expected %v, got %v", i, tc.roundtrip.Ports, got2.Ports) + } + } +} + func TestNodeConversion(t *testing.T) { version, kind, err := newer.Scheme.ObjectVersionAndKind(¤t.Minion{}) if err != nil { diff --git a/pkg/api/v1beta2/defaults.go b/pkg/api/v1beta2/defaults.go index 067ee6b0a05..025a2b128d2 100644 --- a/pkg/api/v1beta2/defaults.go +++ b/pkg/api/v1beta2/defaults.go @@ -68,6 +68,15 @@ func init() { if obj.SessionAffinity == "" { obj.SessionAffinity = AffinityTypeNone } + for i := range obj.Ports { + sp := &obj.Ports[i] + if sp.Protocol == "" { + sp.Protocol = ProtocolTCP + } + if sp.ContainerPort == util.NewIntOrStringFromInt(0) || sp.ContainerPort == util.NewIntOrStringFromString("") { + sp.ContainerPort = util.NewIntOrStringFromInt(sp.Port) + } + } }, func(obj *PodSpec) { if obj.DNSPolicy == "" { diff --git a/pkg/api/v1beta2/defaults_test.go b/pkg/api/v1beta2/defaults_test.go index a478158429a..40652ed57e0 100644 --- a/pkg/api/v1beta2/defaults_test.go +++ b/pkg/api/v1beta2/defaults_test.go @@ -23,6 +23,7 @@ import ( newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { @@ -153,3 +154,35 @@ func TestSetDefaultContainerManifestHostNetwork(t *testing.T) { t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum) } } + +func TestSetDefaultServicePort(t *testing.T) { + // Unchanged if set. + in := ¤t.Service{Ports: []current.ServicePort{{Protocol: "UDP", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(118)}}} + out := roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolUDP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != in.Ports[0].ContainerPort { + t.Errorf("Expected port %d, got %d", in.Ports[0].ContainerPort, out.Ports[0].ContainerPort) + } + + // Defaulted. + in = ¤t.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(0)}}} + out = roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) { + t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort) + } + + // Defaulted. + in = ¤t.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromString("")}}} + out = roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Ports[0].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol) + } + if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) { + t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort) + } +} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 09b0b6ed215..e5c7ff52e4a 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -721,14 +721,21 @@ type Service struct { // Required. Port int `json:"port" description:"port exposed by the service"` + // Optional: The name of the first port. + PortName string `json:"portName,omitempty" description:"the name of the first port; optional"` // Optional: Defaults to "TCP". Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` + // ContainerPort is the name or number of the port on the container to direct traffic to. + // This is useful if the containers the service points to have multiple open ports. + // Optional: If unspecified, the first port on the container will be used. + ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` // This service's labels. Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"` // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` + // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` @@ -736,11 +743,6 @@ type Service struct { // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` - // ContainerPort is the name or number of the port on the container to direct traffic to. - // This is useful if the containers the service points to have multiple open ports. - // Optional: If unspecified, the first port on the container will be used. - ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` - // PortalIP is usually assigned by the master. If specified by the user // we will try to respect it or else fail the request. This field can // not be changed by updates. @@ -753,6 +755,35 @@ type Service struct { // Optional: Supports "ClientIP" and "None". Used to maintain session affinity. SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` + + // Optional: Ports to expose on the service. If this field is + // specified, the legacy fields (Port, PortName, Protocol, and + // ContainerPort) will be overwritten by the first member of this + // array. If this field is not specified, it will be populated from + // the legacy fields. + Ports []ServicePort `json:"ports" description:"ports to be exposed on the service; if this field is specified, the legacy fields (Port, PortName, Protocol, and ContainerPort) will be overwritten by the first member of this array; if this field is not specified, it will be populated from the legacy fields"` +} + +type ServicePort struct { + // Required: The name of this port within the service. This must be a + // DNS_LABEL. All ports within a ServiceSpec must have unique names. + // This maps to the 'Name' field in EndpointPort objects. + Name string `json:"name" description:"the name of this port; optional if only one port is defined"` + + // Optional: The IP protocol for this port. Supports "TCP" and "UDP", + // default is TCP. + Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"` + + // Required: The port that will be exposed. + Port int `json:"port" description:"the port number that is exposed"` + + // Optional: The port number on the target pod to direct traffic to. + // This is useful if the containers the service points to have multiple + // open ports. If this is a string, it will be looked up as a named + // port in the target Pod's container ports. If unspecified, the value + // of Port is used (an identity map) - note this is a different default + // than Service.ContainerPort. + ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` } // EndpointObjectReference is a reference to an object exposing the endpoint diff --git a/pkg/api/v1beta3/defaults.go b/pkg/api/v1beta3/defaults.go index 9e00a234303..27883abe07e 100644 --- a/pkg/api/v1beta3/defaults.go +++ b/pkg/api/v1beta3/defaults.go @@ -52,12 +52,18 @@ func init() { obj.TerminationMessagePath = TerminationMessagePathDefault } }, - func(obj *Service) { - if obj.Spec.Protocol == "" { - obj.Spec.Protocol = ProtocolTCP + func(obj *ServiceSpec) { + if obj.SessionAffinity == "" { + obj.SessionAffinity = AffinityTypeNone } - if obj.Spec.SessionAffinity == "" { - obj.Spec.SessionAffinity = AffinityTypeNone + for i := range obj.Ports { + sp := &obj.Ports[i] + if sp.Protocol == "" { + sp.Protocol = ProtocolTCP + } + if sp.TargetPort == util.NewIntOrStringFromInt(0) || sp.TargetPort == util.NewIntOrStringFromString("") { + sp.TargetPort = util.NewIntOrStringFromInt(sp.Port) + } } }, func(obj *PodSpec) { @@ -97,12 +103,6 @@ func init() { obj.Path = "/" } }, - func(obj *ServiceSpec) { - if obj.TargetPort.Kind == util.IntstrInt && obj.TargetPort.IntVal == 0 || - obj.TargetPort.Kind == util.IntstrString && obj.TargetPort.StrVal == "" { - obj.TargetPort = util.NewIntOrStringFromInt(obj.Port) - } - }, func(obj *NamespaceStatus) { if obj.Phase == "" { obj.Phase = NamespaceActive diff --git a/pkg/api/v1beta3/defaults_test.go b/pkg/api/v1beta3/defaults_test.go index 0bb13446b1c..d0549ee57a4 100644 --- a/pkg/api/v1beta3/defaults_test.go +++ b/pkg/api/v1beta3/defaults_test.go @@ -50,9 +50,6 @@ func TestSetDefaultService(t *testing.T) { svc := ¤t.Service{} obj2 := roundTrip(t, runtime.Object(svc)) svc2 := obj2.(*current.Service) - if svc2.Spec.Protocol != current.ProtocolTCP { - t.Errorf("Expected default protocol :%s, got: %s", current.ProtocolTCP, svc2.Spec.Protocol) - } if svc2.Spec.SessionAffinity != current.AffinityTypeNone { t.Errorf("Expected default sesseion affinity type:%s, got: %s", current.AffinityTypeNone, svc2.Spec.SessionAffinity) } @@ -91,18 +88,62 @@ func TestSetDefaulEndpointsProtocol(t *testing.T) { } func TestSetDefaulServiceTargetPort(t *testing.T) { - in := ¤t.Service{Spec: current.ServiceSpec{Port: 1234}} + in := ¤t.Service{Spec: current.ServiceSpec{Ports: []current.ServicePort{{Port: 1234}}}} obj := roundTrip(t, runtime.Object(in)) out := obj.(*current.Service) - if out.Spec.TargetPort.Kind != util.IntstrInt || out.Spec.TargetPort.IntVal != 1234 { - t.Errorf("Expected TargetPort to be defaulted, got %s", out.Spec.TargetPort) + if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(1234) { + t.Errorf("Expected TargetPort to be defaulted, got %s", out.Spec.Ports[0].TargetPort) } - in = ¤t.Service{Spec: current.ServiceSpec{Port: 1234, TargetPort: util.NewIntOrStringFromInt(5678)}} + in = ¤t.Service{Spec: current.ServiceSpec{Ports: []current.ServicePort{{Port: 1234, TargetPort: util.NewIntOrStringFromInt(5678)}}}} obj = roundTrip(t, runtime.Object(in)) out = obj.(*current.Service) - if out.Spec.TargetPort.Kind != util.IntstrInt || out.Spec.TargetPort.IntVal != 5678 { - t.Errorf("Expected TargetPort to be unchanged, got %s", out.Spec.TargetPort) + if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(5678) { + t.Errorf("Expected TargetPort to be unchanged, got %s", out.Spec.Ports[0].TargetPort) + } +} + +func TestSetDefaultServicePort(t *testing.T) { + // Unchanged if set. + in := ¤t.Service{Spec: current.ServiceSpec{ + Ports: []current.ServicePort{ + {Protocol: "UDP", Port: 9376, TargetPort: util.NewIntOrStringFromString("p")}, + {Protocol: "UDP", Port: 8675, TargetPort: util.NewIntOrStringFromInt(309)}, + }, + }} + out := roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Spec.Ports[0].Protocol != current.ProtocolUDP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Spec.Ports[0].Protocol) + } + if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromString("p") { + t.Errorf("Expected port %d, got %s", in.Spec.Ports[0].Port, out.Spec.Ports[0].TargetPort) + } + if out.Spec.Ports[1].Protocol != current.ProtocolUDP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Spec.Ports[1].Protocol) + } + if out.Spec.Ports[1].TargetPort != util.NewIntOrStringFromInt(309) { + t.Errorf("Expected port %d, got %s", in.Spec.Ports[1].Port, out.Spec.Ports[1].TargetPort) + } + + // Defaulted. + in = ¤t.Service{Spec: current.ServiceSpec{ + Ports: []current.ServicePort{ + {Protocol: "", Port: 9376, TargetPort: util.NewIntOrStringFromString("")}, + {Protocol: "", Port: 8675, TargetPort: util.NewIntOrStringFromInt(0)}, + }, + }} + out = roundTrip(t, runtime.Object(in)).(*current.Service) + if out.Spec.Ports[0].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Spec.Ports[0].Protocol) + } + if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(in.Spec.Ports[0].Port) { + t.Errorf("Expected port %d, got %d", in.Spec.Ports[0].Port, out.Spec.Ports[0].TargetPort) + } + if out.Spec.Ports[1].Protocol != current.ProtocolTCP { + t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Spec.Ports[1].Protocol) + } + if out.Spec.Ports[1].TargetPort != util.NewIntOrStringFromInt(in.Spec.Ports[1].Port) { + t.Errorf("Expected port %d, got %d", in.Spec.Ports[1].Port, out.Spec.Ports[1].TargetPort) } } diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index b49c88666a0..63ef604b07e 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -855,12 +855,8 @@ type ServiceStatus struct{} // ServiceSpec describes the attributes that a user creates on a service type ServiceSpec struct { - // Port is the TCP or UDP port that will be made available to each pod for connecting to the pods - // proxied by this service. - Port int `json:"port" description:"port exposed by the service"` - - // Optional: Supports "TCP" and "UDP". Defaults to "TCP". - Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` + // Required: The list of ports that are exposed by this service. + Ports []ServicePort `json:"ports" description:"ports exposed by the service"` // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` @@ -879,15 +875,31 @@ type ServiceSpec struct { // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` - // TargetPort is the name or number of the port on the container to direct traffic to. - // This is useful if the containers the service points to have multiple open ports. - // Optional: If unspecified, the service port is used (an identity map). - TargetPort util.IntOrString `json:"targetPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"` - // Optional: Supports "ClientIP" and "None". Used to maintain session affinity. SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` } +type ServicePort struct { + // Optional if only one ServicePort is defined on this service: The + // name of this port within the service. This must be a DNS_LABEL. + // All ports within a ServiceSpec must have unique names. This maps to + // the 'Name' field in EndpointPort objects. + Name string `json:"name" description:"the name of this port; optional if only one port is defined"` + + // Optional: The IP protocol for this port. Supports "TCP" and "UDP", + // default is TCP. + Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"` + + // Required: The port that will be exposed by this service. + Port int `json:"port" description:"the port number that is exposed"` + + // Optional: The target port on pods selected by this service. + // If this is a string, it will be looked up as a named port in the + // target Pod's container ports. If this is not specified, the value + // of Port is used (an identity map). + TargetPort util.IntOrString `json:"targetPort" description:"the port to access on the pods targeted by the service; defaults to the service port"` +} + // Service is a named abstraction of software service (for example, mysql) consisting of local port // (for example 3306) that the proxy listens on, and the selector that determines which pods // will answer requests sent through the proxy. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 81c75de660e..5cfa3bad549 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -786,18 +786,12 @@ func ValidateService(service *api.Service) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName).Prefix("metadata")...) - if !util.IsValidPortNum(service.Spec.Port) { - allErrs = append(allErrs, errs.NewFieldInvalid("spec.port", service.Spec.Port, portRangeErrorMsg)) + if len(service.Spec.Ports) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("spec.ports")) } - if len(service.Spec.Protocol) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("spec.protocol")) - } else if !supportedPortProtocols.Has(strings.ToUpper(string(service.Spec.Protocol))) { - allErrs = append(allErrs, errs.NewFieldNotSupported("spec.protocol", service.Spec.Protocol)) - } - if service.Spec.TargetPort.Kind == util.IntstrInt && service.Spec.TargetPort.IntVal != 0 && !util.IsValidPortNum(service.Spec.TargetPort.IntVal) { - allErrs = append(allErrs, errs.NewFieldInvalid("spec.containerPort", service.Spec.Port, portRangeErrorMsg)) - } else if service.Spec.TargetPort.Kind == util.IntstrString && len(service.Spec.TargetPort.StrVal) == 0 { - allErrs = append(allErrs, errs.NewFieldRequired("spec.containerPort")) + allPortNames := util.StringSet{} + for i := range service.Spec.Ports { + allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], i, &allPortNames).PrefixIndex(i).Prefix("spec.ports")...) } if service.Spec.Selector != nil { @@ -827,6 +821,39 @@ func ValidateService(service *api.Service) errs.ValidationErrorList { return allErrs } +func validateServicePort(sp *api.ServicePort, index int, allNames *util.StringSet) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + if len(sp.Name) == 0 { + // Allow empty names if they are the first port (mostly for compat). + if index != 0 { + allErrs = append(allErrs, errs.NewFieldRequired("name")) + } + } else if !util.IsDNS1123Label(sp.Name) { + allErrs = append(allErrs, errs.NewFieldInvalid("name", sp.Name, dns1123LabelErrorMsg)) + } else if allNames.Has(sp.Name) { + allErrs = append(allErrs, errs.NewFieldDuplicate("name", sp.Name)) + } + + if !util.IsValidPortNum(sp.Port) { + allErrs = append(allErrs, errs.NewFieldInvalid("port", sp.Port, portRangeErrorMsg)) + } + + if len(sp.Protocol) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("protocol")) + } else if !supportedPortProtocols.Has(strings.ToUpper(string(sp.Protocol))) { + allErrs = append(allErrs, errs.NewFieldNotSupported("protocol", sp.Protocol)) + } + + if sp.TargetPort != util.NewIntOrStringFromInt(0) && sp.TargetPort != util.NewIntOrStringFromString("") { + if sp.TargetPort.Kind == util.IntstrInt && !util.IsValidPortNum(sp.TargetPort.IntVal) { + allErrs = append(allErrs, errs.NewFieldInvalid("targetPort", sp.TargetPort, portRangeErrorMsg)) + } + } + + return allErrs +} + // ValidateServiceUpdate tests if required fields in the service are set during an update func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -838,6 +865,7 @@ func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErro allErrs = append(allErrs, errs.NewFieldInvalid("spec.portalIP", service.Spec.PortalIP, "field is immutable")) } + allErrs = append(allErrs, ValidateService(service)...) return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 615ff54162d..a7942ec2650 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1223,220 +1223,254 @@ func TestValidatePodUpdate(t *testing.T) { } } +func makeValidService() api.Service { + return api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "valid", + Namespace: "valid", + Labels: map[string]string{}, + Annotations: map[string]string{}, + ResourceVersion: "1", + }, + Spec: api.ServiceSpec{ + Selector: map[string]string{"key": "val"}, + SessionAffinity: "None", + Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}}, + }, + } +} + func TestValidateService(t *testing.T) { testCases := []struct { - name string - makeSvc func(svc *api.Service) // given a basic valid service, each test case can customize it - numErrs int + name string + tweakSvc func(svc *api.Service) // given a basic valid service, each test case can customize it + numErrs int }{ { name: "missing namespace", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Namespace = "" }, numErrs: 1, }, { name: "invalid namespace", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Namespace = "-123" }, numErrs: 1, }, { name: "missing name", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Name = "" }, numErrs: 1, }, { name: "invalid name", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Name = "-123" }, numErrs: 1, }, { name: "too long name", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Name = strings.Repeat("a", 25) }, numErrs: 1, }, { name: "invalid generateName", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.GenerateName = "-123" }, numErrs: 1, }, { name: "too long generateName", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.GenerateName = strings.Repeat("a", 25) }, numErrs: 1, }, { name: "invalid label", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Labels["NoUppercaseOrSpecialCharsLike=Equals"] = "bar" }, numErrs: 1, }, { name: "invalid annotation", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Annotations["NoSpecialCharsLike=Equals"] = "bar" }, numErrs: 1, }, + { + name: "nil selector", + tweakSvc: func(s *api.Service) { + s.Spec.Selector = nil + }, + numErrs: 0, + }, { name: "invalid selector", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.Selector["NoSpecialCharsLike=Equals"] = "bar" }, numErrs: 1, }, { name: "missing session affinity", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.SessionAffinity = "" }, numErrs: 1, }, + { + name: "missing ports", + tweakSvc: func(s *api.Service) { + s.Spec.Ports = nil + }, + numErrs: 1, + }, + { + name: "empty port[0] name", + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Name = "" + }, + numErrs: 0, + }, + { + name: "empty port[1] name", + tweakSvc: func(s *api.Service) { + s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "", Protocol: "TCP", Port: 12345}) + }, + numErrs: 1, + }, + { + name: "invalid port name", + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Name = "INVALID" + }, + numErrs: 1, + }, { name: "missing protocol", - makeSvc: func(s *api.Service) { - s.Spec.Protocol = "" + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Protocol = "" }, numErrs: 1, }, { name: "invalid protocol", - makeSvc: func(s *api.Service) { - s.Spec.Protocol = "INVALID" + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Protocol = "INVALID" }, numErrs: 1, }, { name: "invalid portal ip", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PortalIP = "invalid" }, numErrs: 1, }, { name: "missing port", - makeSvc: func(s *api.Service) { - s.Spec.Port = 0 + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Port = 0 }, numErrs: 1, }, { name: "invalid port", - makeSvc: func(s *api.Service) { - s.Spec.Port = 65536 + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Port = 65536 }, numErrs: 1, }, { - name: "missing targetPort string", - makeSvc: func(s *api.Service) { - s.Spec.TargetPort = util.NewIntOrStringFromString("") - }, - numErrs: 1, - }, - { - name: "invalid targetPort int", - makeSvc: func(s *api.Service) { - s.Spec.TargetPort = util.NewIntOrStringFromInt(65536) + name: "invalid TargetPort int", + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(65536) }, numErrs: 1, }, { name: "invalid publicIPs localhost", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PublicIPs = []string{"127.0.0.1"} }, numErrs: 1, }, { name: "invalid publicIPs", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PublicIPs = []string{"0.0.0.0"} }, numErrs: 1, }, { name: "valid publicIPs host", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PublicIPs = []string{"myhost.mydomain"} }, numErrs: 0, }, { - name: "nil selector", - makeSvc: func(s *api.Service) { - s.Spec.Selector = nil + name: "dup port name", + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Name = "p" + s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p", Port: 12345}) }, - numErrs: 0, + numErrs: 1, }, { name: "valid 1", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { // do nothing }, numErrs: 0, }, { name: "valid 2", - makeSvc: func(s *api.Service) { - s.Spec.Protocol = "UDP" - s.Spec.TargetPort = util.NewIntOrStringFromInt(12345) + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].Protocol = "UDP" + s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(12345) }, numErrs: 0, }, { name: "valid 3", - makeSvc: func(s *api.Service) { - s.Spec.TargetPort = util.NewIntOrStringFromString("http") + tweakSvc: func(s *api.Service) { + s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString("http") }, numErrs: 0, }, { name: "valid portal ip - none ", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PortalIP = "None" }, numErrs: 0, }, { name: "valid portal ip - empty", - makeSvc: func(s *api.Service) { + tweakSvc: func(s *api.Service) { s.Spec.PortalIP = "" + s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString("http") }, numErrs: 0, }, } for _, tc := range testCases { - svc := api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "valid", - Namespace: "valid", - Labels: map[string]string{}, - Annotations: map[string]string{}, - }, - Spec: api.ServiceSpec{ - Selector: map[string]string{"key": "val"}, - SessionAffinity: "None", - Port: 8675, - Protocol: "TCP", - }, - } - tc.makeSvc(&svc) + svc := makeValidService() + tc.tweakSvc(&svc) errs := ValidateService(&svc) if len(errs) != tc.numErrs { t.Errorf("Unexpected error list for case %q: %v", tc.name, utilerrors.NewAggregate(errs)) @@ -2144,172 +2178,85 @@ func TestValidateMinionUpdate(t *testing.T) { } func TestValidateServiceUpdate(t *testing.T) { - tests := []struct { - oldService api.Service - service api.Service - valid bool + testCases := []struct { + name string + tweakSvc func(oldSvc, newSvc *api.Service) // given basic valid services, each test case can customize them + numErrs int }{ - { // 0 - api.Service{}, - api.Service{}, - true}, - { // 1 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo"}}, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "bar"}, - }, false}, - { // 2 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"foo": "bar"}, - }, + { + name: "no change", + tweakSvc: func(oldSvc, newSvc *api.Service) { + // do nothing }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"foo": "baz"}, - }, - }, true}, - { // 3 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - }, + numErrs: 0, + }, + { + name: "change name", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Name += "2" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"foo": "baz"}, - }, - }, true}, - { // 4 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "foo"}, - }, + numErrs: 1, + }, + { + name: "change namespace", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Namespace += "2" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"foo": "baz"}, - }, - }, true}, - { // 5 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Annotations: map[string]string{"bar": "foo"}, - }, + numErrs: 1, + }, + { + name: "change label valid", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Labels["key"] = "other-value" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Annotations: map[string]string{"foo": "baz"}, - }, - }, true}, - { // 6 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - }, - Spec: api.ServiceSpec{ - Selector: map[string]string{"foo": "baz"}, - }, + numErrs: 0, + }, + { + name: "add label", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Labels["key2"] = "value2" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - }, - Spec: api.ServiceSpec{ - Selector: map[string]string{"foo": "baz"}, - }, - }, true}, - { // 7 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "foo"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "127.0.0.1", - }, + numErrs: 0, + }, + { + name: "change portal IP", + tweakSvc: func(oldSvc, newSvc *api.Service) { + oldSvc.Spec.PortalIP = "1.2.3.4" + newSvc.Spec.PortalIP = "8.6.7.5" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "fooobaz"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "new", - }, - }, false}, - { // 8 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "foo"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "127.0.0.1", - }, + numErrs: 1, + }, + { + name: "remove portal IP", + tweakSvc: func(oldSvc, newSvc *api.Service) { + oldSvc.Spec.PortalIP = "1.2.3.4" + newSvc.Spec.PortalIP = "" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "fooobaz"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "", - }, - }, false}, - { // 9 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "foo"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "127.0.0.1", - }, + numErrs: 1, + }, + { + name: "change affinity", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.SessionAffinity = "ClientIP" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"bar": "fooobaz"}, - }, - Spec: api.ServiceSpec{ - PortalIP: "127.0.0.2", - }, - }, false}, - { // 10 - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"foo": "baz"}, - }, + numErrs: 0, + }, + { + name: "remove affinity", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.SessionAffinity = "" }, - api.Service{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Labels: map[string]string{"Foo": "baz"}, - }, - }, true}, + numErrs: 1, + }, } - for i, test := range tests { - test.oldService.ObjectMeta.ResourceVersion = "1" - test.service.ObjectMeta.ResourceVersion = "1" - errs := ValidateServiceUpdate(&test.oldService, &test.service) - if test.valid && len(errs) > 0 { - t.Errorf("%d: Unexpected error: %v", i, errs) - t.Logf("%#v vs %#v", test.oldService.ObjectMeta, test.service.ObjectMeta) - } - if !test.valid && len(errs) == 0 { - t.Errorf("%d: Unexpected non-error", i) + + for _, tc := range testCases { + oldSvc := makeValidService() + newSvc := makeValidService() + tc.tweakSvc(&oldSvc, &newSvc) + errs := ValidateServiceUpdate(&oldSvc, &newSvc) + if len(errs) != tc.numErrs { + t.Errorf("Unexpected error list for case %q: %v", tc.name, utilerrors.NewAggregate(errs)) } } } diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index bafb75492f7..584a6a7ebe6 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -694,7 +694,11 @@ func TestRequestDo(t *testing.T) { func TestDoRequestNewWay(t *testing.T) { reqBody := "request body" - expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} + expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 12345, + TargetPort: util.NewIntOrStringFromInt(12345), + }}}} expectedBody, _ := v1beta2.Codec.Encode(expectedObj) fakeHandler := util.FakeHandler{ StatusCode: 200, @@ -728,7 +732,11 @@ func TestDoRequestNewWay(t *testing.T) { func TestDoRequestNewWayReader(t *testing.T) { reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} reqBodyExpected, _ := v1beta1.Codec.Encode(reqObj) - expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} + expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 12345, + TargetPort: util.NewIntOrStringFromInt(12345), + }}}} expectedBody, _ := v1beta1.Codec.Encode(expectedObj) fakeHandler := util.FakeHandler{ StatusCode: 200, @@ -764,7 +772,11 @@ func TestDoRequestNewWayReader(t *testing.T) { func TestDoRequestNewWayObj(t *testing.T) { reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} reqBodyExpected, _ := v1beta2.Codec.Encode(reqObj) - expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} + expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 12345, + TargetPort: util.NewIntOrStringFromInt(12345), + }}}} expectedBody, _ := v1beta2.Codec.Encode(expectedObj) fakeHandler := util.FakeHandler{ StatusCode: 200, @@ -814,7 +826,11 @@ func TestDoRequestNewWayFile(t *testing.T) { t.Errorf("unexpected error: %v", err) } - expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} + expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 12345, + TargetPort: util.NewIntOrStringFromInt(12345), + }}}} expectedBody, _ := v1beta1.Codec.Encode(expectedObj) fakeHandler := util.FakeHandler{ StatusCode: 200, @@ -855,7 +871,11 @@ func TestWasCreated(t *testing.T) { t.Errorf("unexpected error: %v", err) } - expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} + expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 12345, + TargetPort: util.NewIntOrStringFromInt(12345), + }}}} expectedBody, _ := v1beta1.Codec.Encode(expectedObj) fakeHandler := util.FakeHandler{ StatusCode: 201, diff --git a/pkg/cloudprovider/cloud.go b/pkg/cloudprovider/cloud.go index 7aa1ff7ab11..c50d0627ebb 100644 --- a/pkg/cloudprovider/cloud.go +++ b/pkg/cloudprovider/cloud.go @@ -48,7 +48,7 @@ type TCPLoadBalancer interface { // TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service TCPLoadBalancerExists(name, region string) (bool, error) // CreateTCPLoadBalancer creates a new tcp load balancer. Returns the IP address or hostname of the balancer - CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) + CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error) // UpdateTCPLoadBalancer updates hosts under the specified load balancer. UpdateTCPLoadBalancer(name, region string, hosts []string) error // DeleteTCPLoadBalancer deletes a specified load balancer. diff --git a/pkg/cloudprovider/fake/fake.go b/pkg/cloudprovider/fake/fake.go index 7aa0b8fc24f..624c2d61d86 100644 --- a/pkg/cloudprovider/fake/fake.go +++ b/pkg/cloudprovider/fake/fake.go @@ -29,7 +29,7 @@ type FakeBalancer struct { Name string Region string ExternalIP net.IP - Port int + Ports []int Hosts []string } @@ -95,9 +95,9 @@ func (f *FakeCloud) TCPLoadBalancerExists(name, region string) (bool, error) { // CreateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.CreateTCPLoadBalancer. // It adds an entry "create" into the internal method call record. -func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { +func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error) { f.addCall("create") - f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, port, hosts}) + f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, ports, hosts}) return f.ExternalIP.String(), f.Err } diff --git a/pkg/cloudprovider/gce/gce.go b/pkg/cloudprovider/gce/gce.go index a187bc2d333..9f0c040f83a 100644 --- a/pkg/cloudprovider/gce/gce.go +++ b/pkg/cloudprovider/gce/gce.go @@ -228,15 +228,29 @@ func translateAffinityType(affinityType api.AffinityType) GCEAffinityType { } // CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer. -func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { +func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error) { pool, err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType)) if err != nil { return "", err } + + if len(ports) == 0 { + return "", fmt.Errorf("no ports specified for GCE load balancer") + } + minPort := 65536 + maxPort := 0 + for i := range ports { + if ports[i] < minPort { + minPort = ports[i] + } + if ports[i] > maxPort { + maxPort = ports[i] + } + } req := &compute.ForwardingRule{ Name: name, IPProtocol: "TCP", - PortRange: strconv.Itoa(port), + PortRange: fmt.Sprintf("%d-%d", minPort, maxPort), Target: pool, } if len(externalIP) > 0 { diff --git a/pkg/cloudprovider/openstack/openstack.go b/pkg/cloudprovider/openstack/openstack.go index 2d07106cc8d..9ab41c4dd4d 100644 --- a/pkg/cloudprovider/openstack/openstack.go +++ b/pkg/cloudprovider/openstack/openstack.go @@ -485,8 +485,12 @@ func (lb *LoadBalancer) TCPLoadBalancerExists(name, region string) (bool, error) // a list of regions (from config) and query/create loadbalancers in // each region. -func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinity api.AffinityType) (string, error) { - glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, port, hosts, affinity) +func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinity api.AffinityType) (string, error) { + glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, ports, hosts, affinity) + + if len(ports) > 1 { + return "", fmt.Errorf("multiple ports are not yet supported in openstack load balancers") + } var persistence *vips.SessionPersistence switch affinity { @@ -515,7 +519,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne _, err = members.Create(lb.network, members.CreateOpts{ PoolID: pool.ID, - ProtocolPort: port, + ProtocolPort: ports[0], //TODO: need to handle multi-port Address: addr, }).Extract() if err != nil { @@ -550,7 +554,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne Description: fmt.Sprintf("Kubernetes external service %s", name), Address: externalIP.String(), Protocol: "TCP", - ProtocolPort: port, + ProtocolPort: ports[0], //TODO: need to handle multi-port PoolID: pool.ID, Persistence: persistence, }).Extract() diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index fc5ea5d3101..10112153af9 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -65,7 +65,6 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList) { ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: api.ServiceSpec{ - Protocol: "TCP", SessionAffinity: "None", }, }, diff --git a/pkg/kubectl/cmd/util/helpers_test.go b/pkg/kubectl/cmd/util/helpers_test.go index 142ad04d950..16730ba039f 100644 --- a/pkg/kubectl/cmd/util/helpers_test.go +++ b/pkg/kubectl/cmd/util/helpers_test.go @@ -135,15 +135,11 @@ func TestMerge(t *testing.T) { { kind: "Service", obj: &api.Service{ - Spec: api.ServiceSpec{ - Port: 10, - }, + Spec: api.ServiceSpec{}, }, fragment: `{ "apiVersion": "v1beta1", "port": 0 }`, expected: &api.Service{ Spec: api.ServiceSpec{ - Port: 0, - Protocol: "TCP", SessionAffinity: "None", }, }, @@ -160,7 +156,6 @@ func TestMerge(t *testing.T) { fragment: `{ "apiVersion": "v1beta1", "selector": { "version": "v2" } }`, expected: &api.Service{ Spec: api.ServiceSpec{ - Protocol: "TCP", SessionAffinity: "None", Selector: map[string]string{ "version": "v2", diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 56845674be5..c8189642cdf 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -333,7 +333,15 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api list := strings.Join(service.Spec.PublicIPs, ", ") fmt.Fprintf(out, "Public IPs:\t%s\n", list) } - fmt.Fprintf(out, "Port:\t%d\n", service.Spec.Port) + for i := range service.Spec.Ports { + sp := &service.Spec.Ports[i] + + name := sp.Name + if name == "" { + name = "" + } + fmt.Fprintf(out, "Port:\t%s\t%d/%s\n", name, sp.Port, sp.Protocol) + } fmt.Fprintf(out, "Endpoints:\t%s\n", formatEndpoints(endpoints)) fmt.Fprintf(out, "Session Affinity:\t%s\n", service.Spec.SessionAffinity) if events != nil { diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index cb8414e2cf8..9acc9915dfd 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -228,7 +228,7 @@ func (h *HumanReadablePrinter) validatePrintHandlerFunc(printFunc reflect.Value) var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED"} var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS"} -var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT"} +var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT(S)"} var endpointColumns = []string{"NAME", "ENDPOINTS"} var nodeColumns = []string{"NAME", "LABELS", "STATUS"} var statusColumns = []string{"STATUS"} @@ -390,9 +390,18 @@ func printReplicationControllerList(list *api.ReplicationControllerList, w io.Wr } func printService(svc *api.Service, w io.Writer) error { - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", svc.Name, formatLabels(svc.Labels), - formatLabels(svc.Spec.Selector), svc.Spec.PortalIP, svc.Spec.Port) - return err + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", svc.Name, formatLabels(svc.Labels), + formatLabels(svc.Spec.Selector), svc.Spec.PortalIP, svc.Spec.Ports[0].Port, svc.Spec.Ports[0].Protocol); err != nil { + return err + } + for i := 1; i < len(svc.Spec.Ports); i++ { + // Lay out additional ports. + if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", "", "", "", "", svc.Spec.Ports[i].Port, svc.Spec.Ports[i].Protocol); err != nil { + return err + } + } + + return nil } func printServiceList(list *api.ServiceList, w io.Writer) error { diff --git a/pkg/kubectl/service.go b/pkg/kubectl/service.go index ab4aadaff39..b3a600a7cbb 100644 --- a/pkg/kubectl/service.go +++ b/pkg/kubectl/service.go @@ -71,9 +71,14 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro Labels: labels, }, Spec: api.ServiceSpec{ - Port: port, - Protocol: api.Protocol(params["protocol"]), Selector: selector, + Ports: []api.ServicePort{ + { + Name: "default", + Port: port, + Protocol: api.Protocol(params["protocol"]), + }, + }, }, } targetPort, found := params["target-port"] @@ -82,12 +87,12 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro } if found && len(targetPort) > 0 { if portNum, err := strconv.Atoi(targetPort); err != nil { - service.Spec.TargetPort = util.NewIntOrStringFromString(targetPort) + service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString(targetPort) } else { - service.Spec.TargetPort = util.NewIntOrStringFromInt(portNum) + service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(portNum) } } else { - service.Spec.TargetPort = util.NewIntOrStringFromInt(port) + service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(port) } if params["create-external-load-balancer"] == "true" { service.Spec.CreateExternalLoadBalancer = true diff --git a/pkg/kubectl/service_test.go b/pkg/kubectl/service_test.go index 2c60bb7826a..7d679bc7487 100644 --- a/pkg/kubectl/service_test.go +++ b/pkg/kubectl/service_test.go @@ -46,9 +46,14 @@ func TestGenerateService(t *testing.T) { "foo": "bar", "baz": "blah", }, - Port: 80, - Protocol: "TCP", - TargetPort: util.NewIntOrStringFromInt(1234), + Ports: []api.ServicePort{ + { + Name: "default", + Port: 80, + Protocol: "TCP", + TargetPort: util.NewIntOrStringFromInt(1234), + }, + }, }, }, }, @@ -69,9 +74,14 @@ func TestGenerateService(t *testing.T) { "foo": "bar", "baz": "blah", }, - Port: 80, - Protocol: "UDP", - TargetPort: util.NewIntOrStringFromString("foobar"), + Ports: []api.ServicePort{ + { + Name: "default", + Port: 80, + Protocol: "UDP", + TargetPort: util.NewIntOrStringFromString("foobar"), + }, + }, }, }, }, @@ -97,9 +107,14 @@ func TestGenerateService(t *testing.T) { "foo": "bar", "baz": "blah", }, - Port: 80, - Protocol: "TCP", - TargetPort: util.NewIntOrStringFromInt(1234), + Ports: []api.ServicePort{ + { + Name: "default", + Port: 80, + Protocol: "TCP", + TargetPort: util.NewIntOrStringFromInt(1234), + }, + }, }, }, }, @@ -121,10 +136,15 @@ func TestGenerateService(t *testing.T) { "foo": "bar", "baz": "blah", }, - Port: 80, - Protocol: "UDP", - PublicIPs: []string{"1.2.3.4"}, - TargetPort: util.NewIntOrStringFromString("foobar"), + Ports: []api.ServicePort{ + { + Name: "default", + Port: 80, + Protocol: "UDP", + TargetPort: util.NewIntOrStringFromString("foobar"), + }, + }, + PublicIPs: []string{"1.2.3.4"}, }, }, }, @@ -147,10 +167,15 @@ func TestGenerateService(t *testing.T) { "foo": "bar", "baz": "blah", }, - Port: 80, - Protocol: "UDP", + Ports: []api.ServicePort{ + { + Name: "default", + Port: 80, + Protocol: "UDP", + TargetPort: util.NewIntOrStringFromString("foobar"), + }, + }, PublicIPs: []string{"1.2.3.4"}, - TargetPort: util.NewIntOrStringFromString("foobar"), CreateExternalLoadBalancer: true, }, }, diff --git a/pkg/kubelet/envvars/envvars.go b/pkg/kubelet/envvars/envvars.go index 4848d7673f4..f548a73d39e 100644 --- a/pkg/kubelet/envvars/envvars.go +++ b/pkg/kubelet/envvars/envvars.go @@ -29,19 +29,30 @@ import ( // provided as an argument. func FromServices(services *api.ServiceList) []api.EnvVar { var result []api.EnvVar - for _, service := range services.Items { + for i := range services.Items { + service := &services.Items[i] + // ignore services where PortalIP is "None" or empty // the services passed to this method should be pre-filtered // only services that have the portal IP set should be included here - if !api.IsServiceIPSet(&service) { + if !api.IsServiceIPSet(service) { continue } + // Host name := makeEnvVariableName(service.Name) + "_SERVICE_HOST" result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP}) - // Port + // First port - give it the backwards-compatible name name = makeEnvVariableName(service.Name) + "_SERVICE_PORT" - result = append(result, api.EnvVar{Name: name, Value: strconv.Itoa(service.Spec.Port)}) + result = append(result, api.EnvVar{Name: name, Value: strconv.Itoa(service.Spec.Ports[0].Port)}) + // All named ports (only the first may be unnamed, checked in validation) + for i := range service.Spec.Ports { + sp := &service.Spec.Ports[i] + if sp.Name != "" { + pn := name + "_" + makeEnvVariableName(sp.Name) + result = append(result, api.EnvVar{Name: pn, Value: strconv.Itoa(sp.Port)}) + } + } // Docker-compatible vars. result = append(result, makeLinkVariables(service)...) } @@ -56,33 +67,42 @@ func makeEnvVariableName(str string) string { return strings.ToUpper(strings.Replace(str, "-", "_", -1)) } -func makeLinkVariables(service api.Service) []api.EnvVar { +func makeLinkVariables(service *api.Service) []api.EnvVar { prefix := makeEnvVariableName(service.Name) - protocol := string(api.ProtocolTCP) - if service.Spec.Protocol != "" { - protocol = string(service.Spec.Protocol) - } - portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, service.Spec.Port, strings.ToUpper(protocol)) - return []api.EnvVar{ - { - Name: prefix + "_PORT", - Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, service.Spec.Port), - }, - { - Name: portPrefix, - Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, service.Spec.Port), - }, - { - Name: portPrefix + "_PROTO", - Value: strings.ToLower(protocol), - }, - { - Name: portPrefix + "_PORT", - Value: strconv.Itoa(service.Spec.Port), - }, - { - Name: portPrefix + "_ADDR", - Value: service.Spec.PortalIP, - }, + all := []api.EnvVar{} + for i := range service.Spec.Ports { + sp := &service.Spec.Ports[i] + + protocol := string(api.ProtocolTCP) + if sp.Protocol != "" { + protocol = string(sp.Protocol) + } + if i == 0 { + // Docker special-cases the first port. + all = append(all, api.EnvVar{ + Name: prefix + "_PORT", + Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, sp.Port), + }) + } + portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, sp.Port, strings.ToUpper(protocol)) + all = append(all, []api.EnvVar{ + { + Name: portPrefix, + Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, sp.Port), + }, + { + Name: portPrefix + "_PROTO", + Value: strings.ToLower(protocol), + }, + { + Name: portPrefix + "_PORT", + Value: strconv.Itoa(sp.Port), + }, + { + Name: portPrefix + "_ADDR", + Value: service.Spec.PortalIP, + }, + }...) } + return all } diff --git a/pkg/kubelet/envvars/envvars_test.go b/pkg/kubelet/envvars/envvars_test.go index bff6726c081..44328faf02c 100644 --- a/pkg/kubelet/envvars/envvars_test.go +++ b/pkg/kubelet/envvars/envvars_test.go @@ -30,46 +30,53 @@ func TestFromServices(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "foo-bar"}, Spec: api.ServiceSpec{ - Port: 8080, Selector: map[string]string{"bar": "baz"}, - Protocol: "TCP", PortalIP: "1.2.3.4", + Ports: []api.ServicePort{ + {Port: 8080, Protocol: "TCP"}, + }, }, }, { ObjectMeta: api.ObjectMeta{Name: "abc-123"}, Spec: api.ServiceSpec{ - Port: 8081, Selector: map[string]string{"bar": "baz"}, - Protocol: "UDP", PortalIP: "5.6.7.8", + Ports: []api.ServicePort{ + {Name: "u-d-p", Port: 8081, Protocol: "UDP"}, + {Name: "t-c-p", Port: 8081, Protocol: "TCP"}, + }, }, }, { ObjectMeta: api.ObjectMeta{Name: "q-u-u-x"}, Spec: api.ServiceSpec{ - Port: 8082, Selector: map[string]string{"bar": "baz"}, - Protocol: "TCP", PortalIP: "9.8.7.6", + Ports: []api.ServicePort{ + {Port: 8082, Protocol: "TCP"}, + {Name: "8083", Port: 8083, Protocol: "TCP"}, + }, }, }, { ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-none"}, Spec: api.ServiceSpec{ - Port: 8082, Selector: map[string]string{"bar": "baz"}, - Protocol: "TCP", PortalIP: "None", + Ports: []api.ServicePort{ + {Port: 8082, Protocol: "TCP"}, + }, }, }, { ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-empty"}, Spec: api.ServiceSpec{ - Port: 8082, Selector: map[string]string{"bar": "baz"}, - Protocol: "TCP", PortalIP: "", + Ports: []api.ServicePort{ + {Port: 8082, Protocol: "TCP"}, + }, }, }, }, @@ -85,18 +92,29 @@ func TestFromServices(t *testing.T) { {Name: "FOO_BAR_PORT_8080_TCP_ADDR", Value: "1.2.3.4"}, {Name: "ABC_123_SERVICE_HOST", Value: "5.6.7.8"}, {Name: "ABC_123_SERVICE_PORT", Value: "8081"}, + {Name: "ABC_123_SERVICE_PORT_U_D_P", Value: "8081"}, + {Name: "ABC_123_SERVICE_PORT_T_C_P", Value: "8081"}, {Name: "ABC_123_PORT", Value: "udp://5.6.7.8:8081"}, {Name: "ABC_123_PORT_8081_UDP", Value: "udp://5.6.7.8:8081"}, {Name: "ABC_123_PORT_8081_UDP_PROTO", Value: "udp"}, {Name: "ABC_123_PORT_8081_UDP_PORT", Value: "8081"}, {Name: "ABC_123_PORT_8081_UDP_ADDR", Value: "5.6.7.8"}, + {Name: "ABC_123_PORT_8081_TCP", Value: "tcp://5.6.7.8:8081"}, + {Name: "ABC_123_PORT_8081_TCP_PROTO", Value: "tcp"}, + {Name: "ABC_123_PORT_8081_TCP_PORT", Value: "8081"}, + {Name: "ABC_123_PORT_8081_TCP_ADDR", Value: "5.6.7.8"}, {Name: "Q_U_U_X_SERVICE_HOST", Value: "9.8.7.6"}, {Name: "Q_U_U_X_SERVICE_PORT", Value: "8082"}, + {Name: "Q_U_U_X_SERVICE_PORT_8083", Value: "8083"}, {Name: "Q_U_U_X_PORT", Value: "tcp://9.8.7.6:8082"}, {Name: "Q_U_U_X_PORT_8082_TCP", Value: "tcp://9.8.7.6:8082"}, {Name: "Q_U_U_X_PORT_8082_TCP_PROTO", Value: "tcp"}, {Name: "Q_U_U_X_PORT_8082_TCP_PORT", Value: "8082"}, {Name: "Q_U_U_X_PORT_8082_TCP_ADDR", Value: "9.8.7.6"}, + {Name: "Q_U_U_X_PORT_8083_TCP", Value: "tcp://9.8.7.6:8083"}, + {Name: "Q_U_U_X_PORT_8083_TCP_PROTO", Value: "tcp"}, + {Name: "Q_U_U_X_PORT_8083_TCP_PORT", Value: "8083"}, + {Name: "Q_U_U_X_PORT_8083_TCP_ADDR", Value: "9.8.7.6"}, } if len(vars) != len(expected) { t.Errorf("Expected %d env vars, got: %+v", len(expected), vars) diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index a044575d849..dad00ddb94a 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -1789,97 +1789,139 @@ func TestMakeEnvironmentVariables(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Port: 8081, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8081, + }}, PortalIP: "1.2.3.1", }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Port: 8082, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8082, + }}, PortalIP: "1.2.3.2", }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Port: 8082, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8082, + }}, PortalIP: "None", }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Port: 8082, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8082, + }}, PortalIP: "", }, }, { ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test1"}, Spec: api.ServiceSpec{ - Port: 8083, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8083, + }}, PortalIP: "1.2.3.3", }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "test2"}, Spec: api.ServiceSpec{ - Port: 8084, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8084, + }}, PortalIP: "1.2.3.4", }, }, { ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, Spec: api.ServiceSpec{ - Port: 8085, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8085, + }}, PortalIP: "1.2.3.5", }, }, { ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, Spec: api.ServiceSpec{ - Port: 8085, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8085, + }}, PortalIP: "None", }, }, { ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, Spec: api.ServiceSpec{ - Port: 8085, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8085, + }}, }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "kubernetes"}, Spec: api.ServiceSpec{ - Port: 8086, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8086, + }}, PortalIP: "1.2.3.6", }, }, { ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: "kubernetes"}, Spec: api.ServiceSpec{ - Port: 8087, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8087, + }}, PortalIP: "1.2.3.7", }, }, { ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, Spec: api.ServiceSpec{ - Port: 8088, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8088, + }}, PortalIP: "1.2.3.8", }, }, { ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, Spec: api.ServiceSpec{ - Port: 8088, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8088, + }}, PortalIP: "None", }, }, { ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, Spec: api.ServiceSpec{ - Port: 8088, + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8088, + }}, PortalIP: "", }, }, diff --git a/pkg/master/publish.go b/pkg/master/publish.go index c3bd3bb0c5c..88688358c83 100644 --- a/pkg/master/publish.go +++ b/pkg/master/publish.go @@ -114,11 +114,10 @@ func (m *Master) createMasterServiceIfNeeded(serviceName string, serviceIP net.I Labels: map[string]string{"provider": "kubernetes", "component": "apiserver"}, }, Spec: api.ServiceSpec{ - Port: servicePort, + Ports: []api.ServicePort{{Port: servicePort, Protocol: api.ProtocolTCP}}, // maintained by this code, not by the pod selector Selector: nil, PortalIP: serviceIP.String(), - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, }, } diff --git a/pkg/proxy/config/config_test.go b/pkg/proxy/config/config_test.go index 927b939c1f8..89dac22d5f3 100644 --- a/pkg/proxy/config/config_test.go +++ b/pkg/proxy/config/config_test.go @@ -136,7 +136,10 @@ func TestNewServiceAddedAndNotified(t *testing.T) { handler := NewServiceHandlerMock() handler.Wait(1) config.RegisterHandler(handler) - serviceUpdate := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) + serviceUpdate := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}}, + }) channel <- serviceUpdate handler.ValidateServices(t, serviceUpdate.Services) @@ -147,24 +150,35 @@ func TestServiceAddedRemovedSetAndNotified(t *testing.T) { channel := config.Channel("one") handler := NewServiceHandlerMock() config.RegisterHandler(handler) - serviceUpdate := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) + serviceUpdate := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}}, + }) handler.Wait(1) channel <- serviceUpdate handler.ValidateServices(t, serviceUpdate.Services) - serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) + serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}}, + }) handler.Wait(1) channel <- serviceUpdate2 services := []api.Service{serviceUpdate2.Services[0], serviceUpdate.Services[0]} handler.ValidateServices(t, services) - serviceUpdate3 := CreateServiceUpdate(REMOVE, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}}) + serviceUpdate3 := CreateServiceUpdate(REMOVE, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, + }) handler.Wait(1) channel <- serviceUpdate3 services = []api.Service{serviceUpdate2.Services[0]} handler.ValidateServices(t, services) - serviceUpdate4 := CreateServiceUpdate(SET, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foobar"}, Spec: api.ServiceSpec{Port: 99}}) + serviceUpdate4 := CreateServiceUpdate(SET, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foobar"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 99}}}, + }) handler.Wait(1) channel <- serviceUpdate4 services = []api.Service{serviceUpdate4.Services[0]} @@ -180,8 +194,14 @@ func TestNewMultipleSourcesServicesAddedAndNotified(t *testing.T) { } handler := NewServiceHandlerMock() config.RegisterHandler(handler) - serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) - serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) + serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}}, + }) + serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}}, + }) handler.Wait(2) channelOne <- serviceUpdate1 channelTwo <- serviceUpdate2 @@ -197,8 +217,14 @@ func TestNewMultipleSourcesServicesMultipleHandlersAddedAndNotified(t *testing.T handler2 := NewServiceHandlerMock() config.RegisterHandler(handler) config.RegisterHandler(handler2) - serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) - serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) + serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}}, + }) + serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ + ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}}, + }) handler.Wait(2) handler2.Wait(2) channelOne <- serviceUpdate1 diff --git a/pkg/proxy/loadbalancer.go b/pkg/proxy/loadbalancer.go index 5f46bd639c5..bef117621a4 100644 --- a/pkg/proxy/loadbalancer.go +++ b/pkg/proxy/loadbalancer.go @@ -17,6 +17,7 @@ limitations under the License. package proxy import ( + "fmt" "net" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -27,7 +28,18 @@ import ( type LoadBalancer interface { // NextEndpoint returns the endpoint to handle a request for the given // service-port and source address. - NextEndpoint(service types.NamespacedName, port string, srcAddr net.Addr) (string, error) - NewService(service types.NamespacedName, port string, sessionAffinityType api.AffinityType, stickyMaxAgeMinutes int) error - CleanupStaleStickySessions(service types.NamespacedName, port string) + NextEndpoint(service ServicePortName, srcAddr net.Addr) (string, error) + NewService(service ServicePortName, sessionAffinityType api.AffinityType, stickyMaxAgeMinutes int) error + CleanupStaleStickySessions(service ServicePortName) +} + +// ServicePortName carries a namespace + name + portname. This is the unique +// identfier for a load-balanced service. +type ServicePortName struct { + types.NamespacedName + Port string +} + +func (spn ServicePortName) String() string { + return fmt.Sprintf("%s:%s", spn.NamespacedName.String(), spn.Port) } diff --git a/pkg/proxy/proxier.go b/pkg/proxy/proxier.go index 7a280fd7c5d..e6acd9712df 100644 --- a/pkg/proxy/proxier.go +++ b/pkg/proxy/proxier.go @@ -35,14 +35,13 @@ import ( ) type serviceInfo struct { - portalIP net.IP - portalPort int - protocol api.Protocol - proxyPort int - socket proxySocket - timeout time.Duration - // TODO: make this an net.IP address - publicIP []string + portalIP net.IP + portalPort int + protocol api.Protocol + proxyPort int + socket proxySocket + timeout time.Duration + publicIPs []string // TODO: make this net.IP sessionAffinityType api.AffinityType stickyMaxAgeMinutes int } @@ -59,7 +58,7 @@ type proxySocket interface { // while sessions are active. Close() error // ProxyLoop proxies incoming connections for the specified service to the service endpoints. - ProxyLoop(service types.NamespacedName, info *serviceInfo, proxier *Proxier) + ProxyLoop(service ServicePortName, info *serviceInfo, proxier *Proxier) } // tcpProxySocket implements proxySocket. Close() is implemented by net.Listener. When Close() is called, @@ -68,10 +67,9 @@ type tcpProxySocket struct { net.Listener } -func tryConnect(service types.NamespacedName, srcAddr net.Addr, protocol string, proxier *Proxier) (out net.Conn, err error) { +func tryConnect(service ServicePortName, srcAddr net.Addr, protocol string, proxier *Proxier) (out net.Conn, err error) { for _, retryTimeout := range endpointDialTimeout { - // TODO: support multiple service ports - endpoint, err := proxier.loadBalancer.NextEndpoint(service, "", srcAddr) + endpoint, err := proxier.loadBalancer.NextEndpoint(service, srcAddr) if err != nil { glog.Errorf("Couldn't find an endpoint for %s: %v", service, err) return nil, err @@ -89,7 +87,7 @@ func tryConnect(service types.NamespacedName, srcAddr net.Addr, protocol string, return nil, fmt.Errorf("failed to connect to an endpoint.") } -func (tcp *tcpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *serviceInfo, proxier *Proxier) { +func (tcp *tcpProxySocket) ProxyLoop(service ServicePortName, myInfo *serviceInfo, proxier *Proxier) { for { if info, exists := proxier.getServiceInfo(service); !exists || info != myInfo { // The service port was closed or replaced. @@ -164,7 +162,7 @@ func newClientCache() *clientCache { return &clientCache{clients: map[string]net.Conn{}} } -func (udp *udpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *serviceInfo, proxier *Proxier) { +func (udp *udpProxySocket) ProxyLoop(service ServicePortName, myInfo *serviceInfo, proxier *Proxier) { activeClients := newClientCache() var buffer [4096]byte // 4KiB should be enough for most whole-packets for { @@ -209,7 +207,7 @@ func (udp *udpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *servi } } -func (udp *udpProxySocket) getBackendConn(activeClients *clientCache, cliAddr net.Addr, proxier *Proxier, service types.NamespacedName, timeout time.Duration) (net.Conn, error) { +func (udp *udpProxySocket) getBackendConn(activeClients *clientCache, cliAddr net.Addr, proxier *Proxier, service ServicePortName, timeout time.Duration) (net.Conn, error) { activeClients.mu.Lock() defer activeClients.mu.Unlock() @@ -305,7 +303,7 @@ func newProxySocket(protocol api.Protocol, ip net.IP, port int) (proxySocket, er type Proxier struct { loadBalancer LoadBalancer mu sync.Mutex // protects serviceMap - serviceMap map[types.NamespacedName]*serviceInfo + serviceMap map[ServicePortName]*serviceInfo numProxyLoops int32 // use atomic ops to access this; mostly for testing listenIP net.IP iptables iptables.Interface @@ -347,7 +345,7 @@ func CreateProxier(loadBalancer LoadBalancer, listenIP net.IP, iptables iptables } return &Proxier{ loadBalancer: loadBalancer, - serviceMap: make(map[types.NamespacedName]*serviceInfo), + serviceMap: make(map[ServicePortName]*serviceInfo), listenIP: listenIP, iptables: iptables, hostIP: hostIP, @@ -387,35 +385,32 @@ func (proxier *Proxier) ensurePortals() { // clean up any stale sticky session records in the hash map. func (proxier *Proxier) cleanupStaleStickySessions() { - for name, info := range proxier.serviceMap { - if info.sessionAffinityType != api.AffinityTypeNone { - // TODO: support multiple service ports - proxier.loadBalancer.CleanupStaleStickySessions(name, "") - } + for name := range proxier.serviceMap { + proxier.loadBalancer.CleanupStaleStickySessions(name) } } // This assumes proxier.mu is not locked. -func (proxier *Proxier) stopProxy(service types.NamespacedName, info *serviceInfo) error { +func (proxier *Proxier) stopProxy(service ServicePortName, info *serviceInfo) error { proxier.mu.Lock() defer proxier.mu.Unlock() return proxier.stopProxyInternal(service, info) } // This assumes proxier.mu is locked. -func (proxier *Proxier) stopProxyInternal(service types.NamespacedName, info *serviceInfo) error { +func (proxier *Proxier) stopProxyInternal(service ServicePortName, info *serviceInfo) error { delete(proxier.serviceMap, service) return info.socket.Close() } -func (proxier *Proxier) getServiceInfo(service types.NamespacedName) (*serviceInfo, bool) { +func (proxier *Proxier) getServiceInfo(service ServicePortName) (*serviceInfo, bool) { proxier.mu.Lock() defer proxier.mu.Unlock() info, ok := proxier.serviceMap[service] return info, ok } -func (proxier *Proxier) setServiceInfo(service types.NamespacedName, info *serviceInfo) { +func (proxier *Proxier) setServiceInfo(service ServicePortName, info *serviceInfo) { proxier.mu.Lock() defer proxier.mu.Unlock() proxier.serviceMap[service] = info @@ -424,7 +419,7 @@ func (proxier *Proxier) setServiceInfo(service types.NamespacedName, info *servi // addServiceOnPort starts listening for a new service, returning the serviceInfo. // Pass proxyPort=0 to allocate a random port. The timeout only applies to UDP // connections, for now. -func (proxier *Proxier) addServiceOnPort(service types.NamespacedName, protocol api.Protocol, proxyPort int, timeout time.Duration) (*serviceInfo, error) { +func (proxier *Proxier) addServiceOnPort(service ServicePortName, protocol api.Protocol, proxyPort int, timeout time.Duration) (*serviceInfo, error) { sock, err := newProxySocket(protocol, proxier.listenIP, proxyPort) if err != nil { return nil, err @@ -444,13 +439,13 @@ func (proxier *Proxier) addServiceOnPort(service types.NamespacedName, protocol protocol: protocol, socket: sock, timeout: timeout, - sessionAffinityType: api.AffinityTypeNone, - stickyMaxAgeMinutes: 180, + sessionAffinityType: api.AffinityTypeNone, // default + stickyMaxAgeMinutes: 180, // TODO: paramaterize this in the API. } proxier.setServiceInfo(service, si) glog.V(1).Infof("Proxying for service %q on %s port %d", service, protocol, portNum) - go func(service types.NamespacedName, proxier *Proxier) { + go func(service ServicePortName, proxier *Proxier) { defer util.HandleCrash() atomic.AddInt32(&proxier.numProxyLoops, 1) sock.ProxyLoop(service, si, proxier) @@ -468,51 +463,57 @@ const udpIdleTimeout = 1 * time.Minute // shutdown if missing from the update set. func (proxier *Proxier) OnUpdate(services []api.Service) { glog.V(4).Infof("Received update notice: %+v", services) - activeServices := make(map[types.NamespacedName]bool) // use a map as a set - for _, service := range services { - // if PortalIP is "None" or empty, skip proxying - if !api.IsServiceIPSet(&service) { - continue - } - serviceName := types.NamespacedName{service.Namespace, service.Name} - activeServices[serviceName] = true - info, exists := proxier.getServiceInfo(serviceName) - serviceIP := net.ParseIP(service.Spec.PortalIP) - // TODO: check health of the socket? What if ProxyLoop exited? - if exists && info.portalPort == service.Spec.Port && info.portalIP.Equal(serviceIP) && ipsEqual(service.Spec.PublicIPs, info.publicIP) { - continue - } - if exists { - glog.V(4).Infof("Something changed for service %q: stopping it", serviceName.String()) - err := proxier.closePortal(serviceName, info) - if err != nil { - glog.Errorf("Failed to close portal for %q: %v", serviceName, err) - } - err = proxier.stopProxy(serviceName, info) - if err != nil { - glog.Errorf("Failed to stop service %q: %v", serviceName, err) - } - } - glog.V(1).Infof("Adding new service %q at %s:%d/%s", serviceName, serviceIP, service.Spec.Port, service.Spec.Protocol) - info, err := proxier.addServiceOnPort(serviceName, service.Spec.Protocol, 0, udpIdleTimeout) - if err != nil { - glog.Errorf("Failed to start proxy for %q: %v", serviceName, err) - continue - } - info.portalIP = serviceIP - info.portalPort = service.Spec.Port - info.publicIP = service.Spec.PublicIPs - info.sessionAffinityType = service.Spec.SessionAffinity - // TODO: paramaterize this in the types api file as an attribute of sticky session. For now it's hardcoded to 3 hours. - info.stickyMaxAgeMinutes = 180 - glog.V(4).Infof("info: %+v", info) + activeServices := make(map[ServicePortName]bool) // use a map as a set + for i := range services { + service := &services[i] - err = proxier.openPortal(serviceName, info) - if err != nil { - glog.Errorf("Failed to open portal for %q: %v", serviceName, err) + // if PortalIP is "None" or empty, skip proxying + if !api.IsServiceIPSet(service) { + glog.V(3).Infof("Skipping service %s due to portal IP = %q", types.NamespacedName{service.Namespace, service.Name}, service.Spec.PortalIP) + continue + } + + for i := range service.Spec.Ports { + servicePort := &service.Spec.Ports[i] + + serviceName := ServicePortName{types.NamespacedName{service.Namespace, service.Name}, servicePort.Name} + activeServices[serviceName] = true + serviceIP := net.ParseIP(service.Spec.PortalIP) + info, exists := proxier.getServiceInfo(serviceName) + // TODO: check health of the socket? What if ProxyLoop exited? + if exists && sameConfig(info, service, servicePort) { + // Nothing changed. + continue + } + if exists { + glog.V(4).Infof("Something changed for service %q: stopping it", serviceName) + err := proxier.closePortal(serviceName, info) + if err != nil { + glog.Errorf("Failed to close portal for %q: %v", serviceName, err) + } + err = proxier.stopProxy(serviceName, info) + if err != nil { + glog.Errorf("Failed to stop service %q: %v", serviceName, err) + } + } + glog.V(1).Infof("Adding new service %q at %s:%d/%s", serviceName, serviceIP, servicePort.Port, servicePort.Protocol) + info, err := proxier.addServiceOnPort(serviceName, servicePort.Protocol, 0, udpIdleTimeout) + if err != nil { + glog.Errorf("Failed to start proxy for %q: %v", serviceName, err) + continue + } + info.portalIP = serviceIP + info.portalPort = servicePort.Port + info.publicIPs = service.Spec.PublicIPs + info.sessionAffinityType = service.Spec.SessionAffinity + glog.V(4).Infof("info: %+v", info) + + err = proxier.openPortal(serviceName, info) + if err != nil { + glog.Errorf("Failed to open portal for %q: %v", serviceName, err) + } + proxier.loadBalancer.NewService(serviceName, info.sessionAffinityType, info.stickyMaxAgeMinutes) } - // TODO: support multiple service ports - proxier.loadBalancer.NewService(serviceName, "", info.sessionAffinityType, info.stickyMaxAgeMinutes) } proxier.mu.Lock() defer proxier.mu.Unlock() @@ -531,6 +532,22 @@ func (proxier *Proxier) OnUpdate(services []api.Service) { } } +func sameConfig(info *serviceInfo, service *api.Service, port *api.ServicePort) bool { + if info.protocol != port.Protocol || info.portalPort != port.Port { + return false + } + if !info.portalIP.Equal(net.ParseIP(service.Spec.PortalIP)) { + return false + } + if !ipsEqual(info.publicIPs, service.Spec.PublicIPs) { + return false + } + if info.sessionAffinityType != service.Spec.SessionAffinity { + return false + } + return true +} + func ipsEqual(lhs, rhs []string) bool { if len(lhs) != len(rhs) { return false @@ -543,12 +560,12 @@ func ipsEqual(lhs, rhs []string) bool { return true } -func (proxier *Proxier) openPortal(service types.NamespacedName, info *serviceInfo) error { +func (proxier *Proxier) openPortal(service ServicePortName, info *serviceInfo) error { err := proxier.openOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) if err != nil { return err } - for _, publicIP := range info.publicIP { + for _, publicIP := range info.publicIPs { err = proxier.openOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) if err != nil { return err @@ -557,7 +574,7 @@ func (proxier *Proxier) openPortal(service types.NamespacedName, info *serviceIn return nil } -func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name types.NamespacedName) error { +func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) error { // Handle traffic from containers. args := proxier.iptablesContainerPortalArgs(portalIP, portalPort, protocol, proxyIP, proxyPort, name) existed, err := proxier.iptables.EnsureRule(iptables.TableNAT, iptablesContainerPortalChain, args...) @@ -582,10 +599,10 @@ func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol return nil } -func (proxier *Proxier) closePortal(service types.NamespacedName, info *serviceInfo) error { +func (proxier *Proxier) closePortal(service ServicePortName, info *serviceInfo) error { // Collect errors and report them all at the end. el := proxier.closeOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) - for _, publicIP := range info.publicIP { + for _, publicIP := range info.publicIPs { el = append(el, proxier.closeOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)...) } if len(el) == 0 { @@ -596,7 +613,7 @@ func (proxier *Proxier) closePortal(service types.NamespacedName, info *serviceI return errors.NewAggregate(el) } -func (proxier *Proxier) closeOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name types.NamespacedName) []error { +func (proxier *Proxier) closeOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) []error { el := []error{} // Handle traffic from containers. @@ -675,7 +692,7 @@ var zeroIPv6 = net.ParseIP("::0") var localhostIPv6 = net.ParseIP("::1") // Build a slice of iptables args that are common to from-container and from-host portal rules. -func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, service types.NamespacedName) []string { +func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, service ServicePortName) []string { // This list needs to include all fields as they are eventually spit out // by iptables-save. This is because some systems do not support the // 'iptables -C' arg, and so fall back on parsing iptables-save output. @@ -696,7 +713,7 @@ func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol } // Build a slice of iptables args for a from-container portal rule. -func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service types.NamespacedName) []string { +func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string { args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) // This is tricky. @@ -743,7 +760,7 @@ func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, } // Build a slice of iptables args for a from-host portal rule. -func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service types.NamespacedName) []string { +func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string { args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) // This is tricky. diff --git a/pkg/proxy/proxier_test.go b/pkg/proxy/proxier_test.go index 168cba5a259..bd88f80ccd5 100644 --- a/pkg/proxy/proxier_test.go +++ b/pkg/proxy/proxier_test.go @@ -195,13 +195,13 @@ func waitForNumProxyLoops(t *testing.T, p *Proxier, want int32) { func TestTCPProxy(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -219,13 +219,13 @@ func TestTCPProxy(t *testing.T) { func TestUDPProxy(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: udpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}}, }}, }, }) @@ -241,8 +241,88 @@ func TestUDPProxy(t *testing.T) { waitForNumProxyLoops(t, p, 1) } +func TestMultiPortProxy(t *testing.T) { + lb := NewLoadBalancerRR() + serviceP := ServicePortName{types.NamespacedName{"testnamespace", "echo-p"}, "p"} + serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "echo-q"}, "q"} + lb.OnUpdate([]api.Endpoints{{ + ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, + Subsets: []api.EndpointSubset{{ + Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, + Ports: []api.EndpointPort{{Name: "p", Protocol: "TCP", Port: tcpServerPort}}, + }}, + }, { + ObjectMeta: api.ObjectMeta{Name: serviceQ.Name, Namespace: serviceQ.Namespace}, + Subsets: []api.EndpointSubset{{ + Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, + Ports: []api.EndpointPort{{Name: "q", Protocol: "UDP", Port: udpServerPort}}, + }}, + }}) + + p := CreateProxier(lb, net.ParseIP("0.0.0.0"), &fakeIptables{}, net.ParseIP("127.0.0.1")) + waitForNumProxyLoops(t, p, 0) + + svcInfoP, err := p.addServiceOnPort(serviceP, "TCP", 0, time.Second) + if err != nil { + t.Fatalf("error adding new service: %#v", err) + } + testEchoTCP(t, "127.0.0.1", svcInfoP.proxyPort) + waitForNumProxyLoops(t, p, 1) + + svcInfoQ, err := p.addServiceOnPort(serviceQ, "UDP", 0, time.Second) + if err != nil { + t.Fatalf("error adding new service: %#v", err) + } + testEchoUDP(t, "127.0.0.1", svcInfoQ.proxyPort) + waitForNumProxyLoops(t, p, 2) +} + +func TestMultiPortOnUpdate(t *testing.T) { + lb := NewLoadBalancerRR() + serviceP := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} + serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "q"} + serviceX := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "x"} + + p := CreateProxier(lb, net.ParseIP("0.0.0.0"), &fakeIptables{}, net.ParseIP("127.0.0.1")) + waitForNumProxyLoops(t, p, 0) + + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: 80, + Protocol: "TCP", + }, { + Name: "q", + Port: 81, + Protocol: "UDP", + }}}, + }}) + waitForNumProxyLoops(t, p, 2) + svcInfo, exists := p.getServiceInfo(serviceP) + if !exists { + t.Fatalf("can't find serviceInfo for %s", serviceP) + } + if svcInfo.portalIP.String() != "1.2.3.4" || svcInfo.portalPort != 80 || svcInfo.protocol != "TCP" { + t.Errorf("unexpected serviceInfo for %s: %#v", serviceP, svcInfo) + } + + svcInfo, exists = p.getServiceInfo(serviceQ) + if !exists { + t.Fatalf("can't find serviceInfo for %s", serviceQ) + } + if svcInfo.portalIP.String() != "1.2.3.4" || svcInfo.portalPort != 81 || svcInfo.protocol != "UDP" { + t.Errorf("unexpected serviceInfo for %s: %#v", serviceQ, svcInfo) + } + + svcInfo, exists = p.getServiceInfo(serviceX) + if exists { + t.Fatalf("found unwanted serviceInfo for %s: %#v", serviceX, svcInfo) + } +} + // Helper: Stops the proxy for the named service. -func stopProxyByName(proxier *Proxier, service types.NamespacedName) error { +func stopProxyByName(proxier *Proxier, service ServicePortName) error { info, found := proxier.getServiceInfo(service) if !found { return fmt.Errorf("unknown service: %s", service) @@ -252,13 +332,13 @@ func stopProxyByName(proxier *Proxier, service types.NamespacedName) error { func TestTCPProxyStop(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -287,13 +367,13 @@ func TestTCPProxyStop(t *testing.T) { func TestUDPProxyStop(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: udpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}}, }}, }, }) @@ -322,13 +402,13 @@ func TestUDPProxyStop(t *testing.T) { func TestTCPProxyUpdateDelete(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -356,13 +436,13 @@ func TestTCPProxyUpdateDelete(t *testing.T) { func TestUDPProxyUpdateDelete(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: udpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}}, }}, }, }) @@ -390,13 +470,13 @@ func TestUDPProxyUpdateDelete(t *testing.T) { func TestTCPProxyUpdateDeleteUpdate(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -420,9 +500,15 @@ func TestTCPProxyUpdateDeleteUpdate(t *testing.T) { t.Fatalf(err.Error()) } waitForNumProxyLoops(t, p, 0) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, - }) + + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.proxyPort, + Protocol: "TCP", + }}}, + }}) svcInfo, exists := p.getServiceInfo(service) if !exists { t.Fatalf("can't find serviceInfo for %s", service) @@ -433,13 +519,13 @@ func TestTCPProxyUpdateDeleteUpdate(t *testing.T) { func TestUDPProxyUpdateDeleteUpdate(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: udpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}}, }}, }, }) @@ -463,9 +549,15 @@ func TestUDPProxyUpdateDeleteUpdate(t *testing.T) { t.Fatalf(err.Error()) } waitForNumProxyLoops(t, p, 0) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, - }) + + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.proxyPort, + Protocol: "UDP", + }}}, + }}) svcInfo, exists := p.getServiceInfo(service) if !exists { t.Fatalf("can't find serviceInfo") @@ -476,13 +568,13 @@ func TestUDPProxyUpdateDeleteUpdate(t *testing.T) { func TestTCPProxyUpdatePort(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -497,9 +589,14 @@ func TestTCPProxyUpdatePort(t *testing.T) { testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) waitForNumProxyLoops(t, p, 1) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: 99, + Protocol: "TCP", + }}}, + }}) // Wait for the socket to actually get free. if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil { t.Fatalf(err.Error()) @@ -516,13 +613,13 @@ func TestTCPProxyUpdatePort(t *testing.T) { func TestUDPProxyUpdatePort(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: udpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}}, }}, }, }) @@ -536,9 +633,14 @@ func TestUDPProxyUpdatePort(t *testing.T) { } waitForNumProxyLoops(t, p, 1) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: 99, + Protocol: "UDP", + }}}, + }}) // Wait for the socket to actually get free. if err := waitForClosedPortUDP(p, svcInfo.proxyPort); err != nil { t.Fatalf(err.Error()) @@ -553,13 +655,13 @@ func TestUDPProxyUpdatePort(t *testing.T) { func TestProxyUpdatePublicIPs(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -574,9 +676,18 @@ func TestProxyUpdatePublicIPs(t *testing.T) { testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) waitForNumProxyLoops(t, p, 1) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.portalPort, Protocol: "TCP", PortalIP: svcInfo.portalIP.String(), PublicIPs: []string{"4.3.2.1"}}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.portalPort, + Protocol: "TCP", + }}, + PortalIP: svcInfo.portalIP.String(), + PublicIPs: []string{"4.3.2.1"}, + }, + }}) // Wait for the socket to actually get free. if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil { t.Fatalf(err.Error()) @@ -593,13 +704,13 @@ func TestProxyUpdatePublicIPs(t *testing.T) { func TestProxyUpdatePortal(t *testing.T) { lb := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "echo") + service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"} lb.OnUpdate([]api.Endpoints{ { ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, - Ports: []api.EndpointPort{{Port: tcpServerPort}}, + Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}}, }}, }, }) @@ -614,33 +725,40 @@ func TestProxyUpdatePortal(t *testing.T) { testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) waitForNumProxyLoops(t, p, 1) - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP"}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "", Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.proxyPort, + Protocol: "TCP", + }}}, + }}) _, exists := p.getServiceInfo(service) - if exists { - t.Fatalf("service without portalIP should not be included in the proxy") - } - - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: ""}, Status: api.ServiceStatus{}}, - }) - _, exists = p.getServiceInfo(service) if exists { t.Fatalf("service with empty portalIP should not be included in the proxy") } - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "None"}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "None", Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.proxyPort, + Protocol: "TCP", + }}}, + }}) _, exists = p.getServiceInfo(service) if exists { t.Fatalf("service with 'None' as portalIP should not be included in the proxy") } - p.OnUpdate([]api.Service{ - {ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, - }) + p.OnUpdate([]api.Service{{ + ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{ + Name: "p", + Port: svcInfo.proxyPort, + Protocol: "TCP", + }}}, + }}) svcInfo, exists = p.getServiceInfo(service) if !exists { t.Fatalf("service with portalIP set not found in the proxy") diff --git a/pkg/proxy/roundrobin.go b/pkg/proxy/roundrobin.go index dfa7028a216..c1bd4ab8354 100644 --- a/pkg/proxy/roundrobin.go +++ b/pkg/proxy/roundrobin.go @@ -50,20 +50,10 @@ type affinityPolicy struct { ttlMinutes int } -// servicePort is the type that the balancer uses to key stored state. -type servicePort struct { - types.NamespacedName - port string -} - -func (sp servicePort) String() string { - return fmt.Sprintf("%s:%s", sp.NamespacedName, sp.port) -} - // LoadBalancerRR is a round-robin load balancer. type LoadBalancerRR struct { lock sync.RWMutex - services map[servicePort]*balancerState + services map[ServicePortName]*balancerState } // Ensure this implements LoadBalancer. @@ -86,13 +76,11 @@ func newAffinityPolicy(affinityType api.AffinityType, ttlMinutes int) *affinityP // NewLoadBalancerRR returns a new LoadBalancerRR. func NewLoadBalancerRR() *LoadBalancerRR { return &LoadBalancerRR{ - services: map[servicePort]*balancerState{}, + services: map[ServicePortName]*balancerState{}, } } -func (lb *LoadBalancerRR) NewService(service types.NamespacedName, port string, affinityType api.AffinityType, ttlMinutes int) error { - svcPort := servicePort{service, port} - +func (lb *LoadBalancerRR) NewService(svcPort ServicePortName, affinityType api.AffinityType, ttlMinutes int) error { lb.lock.Lock() defer lb.lock.Unlock() lb.newServiceInternal(svcPort, affinityType, ttlMinutes) @@ -100,7 +88,7 @@ func (lb *LoadBalancerRR) NewService(service types.NamespacedName, port string, } // This assumes that lb.lock is already held. -func (lb *LoadBalancerRR) newServiceInternal(svcPort servicePort, affinityType api.AffinityType, ttlMinutes int) *balancerState { +func (lb *LoadBalancerRR) newServiceInternal(svcPort ServicePortName, affinityType api.AffinityType, ttlMinutes int) *balancerState { if ttlMinutes == 0 { ttlMinutes = 180 //default to 3 hours if not specified. Should 0 be unlimeted instead???? } @@ -123,9 +111,7 @@ func isSessionAffinity(affinity *affinityPolicy) bool { // NextEndpoint returns a service endpoint. // The service endpoint is chosen using the round-robin algorithm. -func (lb *LoadBalancerRR) NextEndpoint(service types.NamespacedName, port string, srcAddr net.Addr) (string, error) { - svcPort := servicePort{service, port} - +func (lb *LoadBalancerRR) NextEndpoint(svcPort ServicePortName, srcAddr net.Addr) (string, error) { // Coarse locking is simple. We can get more fine-grained if/when we // can prove it matters. lb.lock.Lock() @@ -202,7 +188,7 @@ func flattenValidEndpoints(endpoints []hostPortPair) []string { } // Remove any session affinity records associated to a particular endpoint (for example when a pod goes down). -func removeSessionAffinityByEndpoint(state *balancerState, svcPort servicePort, endpoint string) { +func removeSessionAffinityByEndpoint(state *balancerState, svcPort ServicePortName, endpoint string) { for _, affinity := range state.affinity.affinityMap { if affinity.endpoint == endpoint { glog.V(4).Infof("Removing client: %s from affinityMap for service %q", affinity.endpoint, svcPort) @@ -214,7 +200,7 @@ func removeSessionAffinityByEndpoint(state *balancerState, svcPort servicePort, // Loop through the valid endpoints and then the endpoints associated with the Load Balancer. // Then remove any session affinity records that are not in both lists. // This assumes the lb.lock is held. -func (lb *LoadBalancerRR) updateAffinityMap(svcPort servicePort, newEndpoints []string) { +func (lb *LoadBalancerRR) updateAffinityMap(svcPort ServicePortName, newEndpoints []string) { allEndpoints := map[string]int{} for _, newEndpoint := range newEndpoints { allEndpoints[newEndpoint] = 1 @@ -238,7 +224,7 @@ func (lb *LoadBalancerRR) updateAffinityMap(svcPort servicePort, newEndpoints [] // Registered endpoints are updated if found in the update set or // unregistered if missing from the update set. func (lb *LoadBalancerRR) OnUpdate(allEndpoints []api.Endpoints) { - registeredEndpoints := make(map[servicePort]bool) + registeredEndpoints := make(map[ServicePortName]bool) lb.lock.Lock() defer lb.lock.Unlock() @@ -262,7 +248,7 @@ func (lb *LoadBalancerRR) OnUpdate(allEndpoints []api.Endpoints) { } for portname := range portsToEndpoints { - svcPort := servicePort{types.NamespacedName{svcEndpoints.Namespace, svcEndpoints.Name}, portname} + svcPort := ServicePortName{types.NamespacedName{svcEndpoints.Namespace, svcEndpoints.Name}, portname} state, exists := lb.services[svcPort] curEndpoints := []string{} if state != nil { @@ -305,15 +291,12 @@ func slicesEquiv(lhs, rhs []string) bool { return false } -func (lb *LoadBalancerRR) CleanupStaleStickySessions(service types.NamespacedName, port string) { - svcPort := servicePort{service, port} - +func (lb *LoadBalancerRR) CleanupStaleStickySessions(svcPort ServicePortName) { lb.lock.Lock() defer lb.lock.Unlock() state, exists := lb.services[svcPort] if !exists { - glog.Warning("CleanupStaleStickySessions called for non-existent balancer key %q", svcPort) return } for ip, affinity := range state.affinity.affinityMap { diff --git a/pkg/proxy/roundrobin_test.go b/pkg/proxy/roundrobin_test.go index 4a4e00aa462..a4bc1a3c699 100644 --- a/pkg/proxy/roundrobin_test.go +++ b/pkg/proxy/roundrobin_test.go @@ -67,8 +67,8 @@ func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) { loadBalancer := NewLoadBalancerRR() var endpoints []api.Endpoints loadBalancer.OnUpdate(endpoints) - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "does-not-exist", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "does-not-exist"} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil { t.Errorf("Didn't fail with non-existent service") } @@ -77,20 +77,20 @@ func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) { } } -func expectEndpoint(t *testing.T, loadBalancer *LoadBalancerRR, service types.NamespacedName, port string, expected string, netaddr net.Addr) { - endpoint, err := loadBalancer.NextEndpoint(service, port, netaddr) +func expectEndpoint(t *testing.T, loadBalancer *LoadBalancerRR, service ServicePortName, expected string, netaddr net.Addr) { + endpoint, err := loadBalancer.NextEndpoint(service, netaddr) if err != nil { - t.Errorf("Didn't find a service for %s:%s, expected %s, failed with: %v", service, port, expected, err) + t.Errorf("Didn't find a service for %s, expected %s, failed with: %v", service, expected, err) } if endpoint != expected { - t.Errorf("Didn't get expected endpoint for service %s:%s client %v, expected %s, got: %s", service, port, netaddr, expected, endpoint) + t.Errorf("Didn't get expected endpoint for service %s client %v, expected %s, got: %s", service, netaddr, expected, endpoint) } } func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) { loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } @@ -103,10 +103,10 @@ func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) { }}, } loadBalancer.OnUpdate(endpoints) - expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) - expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) - expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) - expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) + expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil) + expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil) + expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil) + expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil) } func stringsInSlice(haystack []string, needles ...string) bool { @@ -127,8 +127,8 @@ func stringsInSlice(haystack []string, needles ...string) bool { func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) { loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } @@ -142,26 +142,27 @@ func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints + shuffledEndpoints := loadBalancer.services[service].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint:1", "endpoint:2", "endpoint:3") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], nil) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], nil) } func TestLoadBalanceWorksWithMultipleEndpointsMultiplePorts(t *testing.T) { loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) + serviceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"} + serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "q"} + endpoint, err := loadBalancer.NextEndpoint(serviceP, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ - ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, Subsets: []api.EndpointSubset{ { Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}}, @@ -175,35 +176,36 @@ func TestLoadBalanceWorksWithMultipleEndpointsMultiplePorts(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints + shuffledEndpoints := loadBalancer.services[serviceP].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:1", "endpoint3:3") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[2], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) - shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints + shuffledEndpoints = loadBalancer.services[serviceQ].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint1:2", "endpoint2:2", "endpoint3:4") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[2], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[2], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) } func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) + serviceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"} + serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "q"} + endpoint, err := loadBalancer.NextEndpoint(serviceP, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ - ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, Subsets: []api.EndpointSubset{ { Addresses: []api.EndpointAddress{{IP: "endpoint1"}}, @@ -221,28 +223,28 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints + shuffledEndpoints := loadBalancer.services[serviceP].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:2", "endpoint3:3") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[2], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) - shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints + shuffledEndpoints = loadBalancer.services[serviceQ].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint1:10", "endpoint2:20", "endpoint3:30") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[2], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[2], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) // Then update the configuration with one fewer endpoints, make sure // we start in the beginning again endpoints[0] = api.Endpoints{ - ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, + ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, Subsets: []api.EndpointSubset{ { Addresses: []api.EndpointAddress{{IP: "endpoint4"}}, @@ -256,29 +258,29 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints = loadBalancer.services[servicePort{service, "p"}].endpoints + shuffledEndpoints = loadBalancer.services[serviceP].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint4:4", "endpoint5:5") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil) - shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints + shuffledEndpoints = loadBalancer.services[serviceQ].endpoints if !stringsInSlice(shuffledEndpoints, "endpoint4:40", "endpoint5:50") { t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) } - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) - expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil) + expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil) // Clear endpoints - endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: nil} + endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, Subsets: nil} loadBalancer.OnUpdate(endpoints) - endpoint, err = loadBalancer.NextEndpoint(service, "p", nil) + endpoint, err = loadBalancer.NextEndpoint(serviceP, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } @@ -286,15 +288,15 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) { loadBalancer := NewLoadBalancerRR() - fooService := types.NewNamespacedNameOrDie("testnamespace", "foo") - barService := types.NewNamespacedNameOrDie("testnamespace", "bar") - endpoint, err := loadBalancer.NextEndpoint(fooService, "p", nil) + fooServiceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"} + barServiceP := ServicePortName{types.NamespacedName{"testnamespace", "bar"}, "p"} + endpoint, err := loadBalancer.NextEndpoint(fooServiceP, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } endpoints := make([]api.Endpoints, 2) endpoints[0] = api.Endpoints{ - ObjectMeta: api.ObjectMeta{Name: fooService.Name, Namespace: fooService.Namespace}, + ObjectMeta: api.ObjectMeta{Name: fooServiceP.Name, Namespace: fooServiceP.Namespace}, Subsets: []api.EndpointSubset{ { Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}, {IP: "endpoint3"}}, @@ -303,7 +305,7 @@ func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) { }, } endpoints[1] = api.Endpoints{ - ObjectMeta: api.ObjectMeta{Name: barService.Name, Namespace: barService.Namespace}, + ObjectMeta: api.ObjectMeta{Name: barServiceP.Name, Namespace: barServiceP.Namespace}, Subsets: []api.EndpointSubset{ { Addresses: []api.EndpointAddress{{IP: "endpoint4"}, {IP: "endpoint5"}, {IP: "endpoint6"}}, @@ -312,54 +314,54 @@ func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledFooEndpoints := loadBalancer.services[servicePort{fooService, "p"}].endpoints - expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[0], nil) - expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[1], nil) - expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[2], nil) - expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[0], nil) - expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[1], nil) + shuffledFooEndpoints := loadBalancer.services[fooServiceP].endpoints + expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[0], nil) + expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[1], nil) + expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[2], nil) + expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[0], nil) + expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[1], nil) - shuffledBarEndpoints := loadBalancer.services[servicePort{barService, "p"}].endpoints - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) + shuffledBarEndpoints := loadBalancer.services[barServiceP].endpoints + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil) // Then update the configuration by removing foo loadBalancer.OnUpdate(endpoints[1:]) - endpoint, err = loadBalancer.NextEndpoint(fooService, "p", nil) + endpoint, err = loadBalancer.NextEndpoint(fooServiceP, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } // but bar is still there, and we continue RR from where we left off. - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) - expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil) + expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil) } func TestStickyLoadBalanceWorksWithSingleEndpoint(t *testing.T) { client1 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) + loadBalancer.NewService(service, api.AffinityTypeClientIP, 0) endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: []api.EndpointSubset{{Addresses: []api.EndpointAddress{{IP: "endpoint"}}, Ports: []api.EndpointPort{{Port: 1}}}}, } loadBalancer.OnUpdate(endpoints) - expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client1) - expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client1) - expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client2) - expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client2) + expectEndpoint(t, loadBalancer, service, "endpoint:1", client1) + expectEndpoint(t, loadBalancer, service, "endpoint:1", client1) + expectEndpoint(t, loadBalancer, service, "endpoint:1", client2) + expectEndpoint(t, loadBalancer, service, "endpoint:1", client2) } func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) { @@ -367,13 +369,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) { client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) + loadBalancer.NewService(service, api.AffinityTypeClientIP, 0) endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, @@ -385,15 +387,15 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) + shuffledEndpoints := loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) } func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) { @@ -401,13 +403,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) { client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(service, "", api.AffinityTypeNone, 0) + loadBalancer.NewService(service, api.AffinityTypeNone, 0) endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, @@ -420,15 +422,15 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client1) + shuffledEndpoints := loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client1) } func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { @@ -439,13 +441,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { client5 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 5), Port: 0} client6 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 6), Port: 0} loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) + loadBalancer.NewService(service, api.AffinityTypeClientIP, 0) endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, @@ -457,14 +459,14 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) + shuffledEndpoints := loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) client1Endpoint := shuffledEndpoints[0] - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) client2Endpoint := shuffledEndpoints[1] - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3) client3Endpoint := shuffledEndpoints[2] endpoints[0] = api.Endpoints{ @@ -477,7 +479,7 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints + shuffledEndpoints = loadBalancer.services[service].endpoints if client1Endpoint == "endpoint:3" { client1Endpoint = shuffledEndpoints[0] } else if client2Endpoint == "endpoint:3" { @@ -485,9 +487,9 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { } else if client3Endpoint == "endpoint:3" { client3Endpoint = shuffledEndpoints[0] } - expectEndpoint(t, loadBalancer, service, "", client1Endpoint, client1) - expectEndpoint(t, loadBalancer, service, "", client2Endpoint, client2) - expectEndpoint(t, loadBalancer, service, "", client3Endpoint, client3) + expectEndpoint(t, loadBalancer, service, client1Endpoint, client1) + expectEndpoint(t, loadBalancer, service, client2Endpoint, client2) + expectEndpoint(t, loadBalancer, service, client3Endpoint, client3) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, @@ -499,13 +501,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", client1Endpoint, client1) - expectEndpoint(t, loadBalancer, service, "", client2Endpoint, client2) - expectEndpoint(t, loadBalancer, service, "", client3Endpoint, client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client4) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client5) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client6) + shuffledEndpoints = loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, client1Endpoint, client1) + expectEndpoint(t, loadBalancer, service, client2Endpoint, client2) + expectEndpoint(t, loadBalancer, service, client3Endpoint, client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client4) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client5) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client6) } func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { @@ -513,13 +515,13 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} loadBalancer := NewLoadBalancerRR() - service := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(service, "", nil) + service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) + loadBalancer.NewService(service, api.AffinityTypeClientIP, 0) endpoints := make([]api.Endpoints, 1) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, @@ -531,14 +533,14 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) + shuffledEndpoints := loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) // Then update the configuration with one fewer endpoints, make sure // we start in the beginning again endpoints[0] = api.Endpoints{ @@ -551,19 +553,19 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { }, } loadBalancer.OnUpdate(endpoints) - shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) - expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) + shuffledEndpoints = loadBalancer.services[service].endpoints + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) + expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2) // Clear endpoints endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: nil} loadBalancer.OnUpdate(endpoints) - endpoint, err = loadBalancer.NextEndpoint(service, "", nil) + endpoint, err = loadBalancer.NextEndpoint(service, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } @@ -574,12 +576,12 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) { client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} loadBalancer := NewLoadBalancerRR() - fooService := types.NewNamespacedNameOrDie("testnamespace", "foo") - endpoint, err := loadBalancer.NextEndpoint(fooService, "", nil) + fooService := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""} + endpoint, err := loadBalancer.NextEndpoint(fooService, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } - loadBalancer.NewService(fooService, "", api.AffinityTypeClientIP, 0) + loadBalancer.NewService(fooService, api.AffinityTypeClientIP, 0) endpoints := make([]api.Endpoints, 2) endpoints[0] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: fooService.Name, Namespace: fooService.Namespace}, @@ -590,8 +592,8 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) { }, }, } - barService := types.NewNamespacedNameOrDie("testnamespace", "bar") - loadBalancer.NewService(barService, "", api.AffinityTypeClientIP, 0) + barService := ServicePortName{types.NamespacedName{"testnamespace", "bar"}, ""} + loadBalancer.NewService(barService, api.AffinityTypeClientIP, 0) endpoints[1] = api.Endpoints{ ObjectMeta: api.ObjectMeta{Name: barService.Name, Namespace: barService.Namespace}, Subsets: []api.EndpointSubset{ @@ -603,38 +605,38 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) { } loadBalancer.OnUpdate(endpoints) - shuffledFooEndpoints := loadBalancer.services[servicePort{fooService, ""}].endpoints - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) - expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) + shuffledFooEndpoints := loadBalancer.services[fooService].endpoints + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3) + expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3) - shuffledBarEndpoints := loadBalancer.services[servicePort{barService, ""}].endpoints - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) + shuffledBarEndpoints := loadBalancer.services[barService].endpoints + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2) // Then update the configuration by removing foo loadBalancer.OnUpdate(endpoints[1:]) - endpoint, err = loadBalancer.NextEndpoint(fooService, "", nil) + endpoint, err = loadBalancer.NextEndpoint(fooService, nil) if err == nil || len(endpoint) != 0 { t.Errorf("Didn't fail with non-existent service") } // but bar is still there, and we continue RR from where we left off. - shuffledBarEndpoints = loadBalancer.services[servicePort{barService, ""}].endpoints - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) - expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) + shuffledBarEndpoints = loadBalancer.services[barService].endpoints + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) + expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1) } diff --git a/pkg/registry/etcd/etcd_test.go b/pkg/registry/etcd/etcd_test.go index 3455c3dce9e..1e51b3d5f6c 100644 --- a/pkg/registry/etcd/etcd_test.go +++ b/pkg/registry/etcd/etcd_test.go @@ -576,7 +576,6 @@ func TestEtcdUpdateService(t *testing.T) { Selector: map[string]string{ "baz": "bar", }, - Protocol: "TCP", SessionAffinity: "None", }, } diff --git a/pkg/registry/service/rest.go b/pkg/registry/service/rest.go index 0949146af71..8fd79d0003a 100644 --- a/pkg/registry/service/rest.go +++ b/pkg/registry/service/rest.go @@ -281,10 +281,12 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service if rs.cloud == nil { return fmt.Errorf("requested an external service, but no cloud provider supplied.") } - if service.Spec.Protocol != api.ProtocolTCP { - // TODO: Support UDP here too. - return fmt.Errorf("external load balancers for non TCP services are not currently supported.") + + ports, err := getTCPPorts(service) + if err != nil { + return err } + balancer, ok := rs.cloud.TCPLoadBalancer() if !ok { return fmt.Errorf("the cloud provider does not support external TCP load balancers.") @@ -302,18 +304,17 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service return err } name := rs.getLoadbalancerName(ctx, service) - // TODO: We should be able to rely on valid input, and not do defaulting here. var affinityType api.AffinityType = service.Spec.SessionAffinity if len(service.Spec.PublicIPs) > 0 { for _, publicIP := range service.Spec.PublicIPs { - _, err = balancer.CreateTCPLoadBalancer(name, zone.Region, net.ParseIP(publicIP), service.Spec.Port, hostsFromMinionList(hosts), affinityType) + _, err = balancer.CreateTCPLoadBalancer(name, zone.Region, net.ParseIP(publicIP), ports, hostsFromMinionList(hosts), affinityType) if err != nil { // TODO: have to roll-back any successful calls. return err } } } else { - endpoint, err := balancer.CreateTCPLoadBalancer(name, zone.Region, nil, service.Spec.Port, hostsFromMinionList(hosts), affinityType) + endpoint, err := balancer.CreateTCPLoadBalancer(name, zone.Region, nil, ports, hostsFromMinionList(hosts), affinityType) if err != nil { return err } @@ -322,6 +323,39 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service return nil } +func getTCPPorts(service *api.Service) ([]int, error) { + ports := []int{} + for i := range service.Spec.Ports { + sp := &service.Spec.Ports[i] + if sp.Protocol != api.ProtocolTCP { + // TODO: Support UDP here too. + return nil, fmt.Errorf("external load balancers for non TCP services are not currently supported.") + } + ports = append(ports, sp.Port) + } + return ports, nil +} + +func portsEqual(x, y *api.Service) bool { + xPorts, err := getTCPPorts(x) + if err != nil { + return false + } + yPorts, err := getTCPPorts(y) + if err != nil { + return false + } + if len(xPorts) != len(yPorts) { + return false + } + for i := range xPorts { + if xPorts[i] != yPorts[i] { + return false + } + } + return true +} + func (rs *REST) deleteExternalLoadBalancer(ctx api.Context, service *api.Service) error { if rs.cloud == nil { return fmt.Errorf("requested an external service, but no cloud provider supplied.") @@ -348,21 +382,21 @@ func (rs *REST) deleteExternalLoadBalancer(ctx api.Context, service *api.Service return nil } -func externalLoadBalancerNeedsUpdate(old, new *api.Service) bool { - if !old.Spec.CreateExternalLoadBalancer && !new.Spec.CreateExternalLoadBalancer { +func externalLoadBalancerNeedsUpdate(oldService, newService *api.Service) bool { + if !oldService.Spec.CreateExternalLoadBalancer && !newService.Spec.CreateExternalLoadBalancer { return false } - if old.Spec.CreateExternalLoadBalancer != new.Spec.CreateExternalLoadBalancer || - old.Spec.Port != new.Spec.Port || - old.Spec.SessionAffinity != new.Spec.SessionAffinity || - old.Spec.Protocol != new.Spec.Protocol { + if oldService.Spec.CreateExternalLoadBalancer != newService.Spec.CreateExternalLoadBalancer { return true } - if len(old.Spec.PublicIPs) != len(new.Spec.PublicIPs) { + if !portsEqual(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity { return true } - for i := range old.Spec.PublicIPs { - if old.Spec.PublicIPs[i] != new.Spec.PublicIPs[i] { + if len(oldService.Spec.PublicIPs) != len(newService.Spec.PublicIPs) { + return true + } + for i := range oldService.Spec.PublicIPs { + if oldService.Spec.PublicIPs[i] != newService.Spec.PublicIPs[i] { return true } } diff --git a/pkg/registry/service/rest_test.go b/pkg/registry/service/rest_test.go index 0362247d3a4..6857af95f67 100644 --- a/pkg/registry/service/rest_test.go +++ b/pkg/registry/service/rest_test.go @@ -17,6 +17,8 @@ limitations under the License. package service import ( + "bytes" + "encoding/gob" "fmt" "net" "strings" @@ -52,6 +54,16 @@ func makeIPNet(t *testing.T) *net.IPNet { return net } +func deepCloneService(svc *api.Service) *api.Service { + buff := new(bytes.Buffer) + enc := gob.NewEncoder(buff) + dec := gob.NewDecoder(buff) + enc.Encode(svc) + result := new(api.Service) + dec.Decode(result) + return result +} + func TestServiceRegistryCreate(t *testing.T) { storage, registry, fakeCloud := NewTestREST(t, nil) storage.portalMgr.randomAttempts = 0 @@ -59,14 +71,19 @@ func TestServiceRegistryCreate(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() - created_svc, _ := storage.Create(ctx, svc) + created_svc, err := storage.Create(ctx, svc) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } created_service := created_svc.(*api.Service) if !api.HasObjectMetaSystemFieldValues(&created_service.ObjectMeta) { t.Errorf("storage did not populate object meta field values") @@ -98,18 +115,22 @@ func TestServiceStorageValidatesCreate(t *testing.T) { "empty ID": { ObjectMeta: api.ObjectMeta{Name: ""}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }, "empty port": { ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Protocol: api.ProtocolTCP, + }}, }, }, } @@ -122,7 +143,6 @@ func TestServiceStorageValidatesCreate(t *testing.T) { if !errors.IsInvalid(err) { t.Errorf("Expected to get an invalid resource error, got %v", err) } - } } @@ -132,8 +152,11 @@ func TestServiceRegistryUpdate(t *testing.T) { svc, err := registry.CreateService(ctx, &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz1"}, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }) @@ -145,10 +168,12 @@ func TestServiceRegistryUpdate(t *testing.T) { Name: "foo", ResourceVersion: svc.ResourceVersion}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz2"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }) if err != nil { @@ -175,27 +200,34 @@ func TestServiceStorageValidatesUpdate(t *testing.T) { registry.CreateService(ctx, &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }) failureCases := map[string]api.Service{ "empty ID": { ObjectMeta: api.ObjectMeta{Name: ""}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }, "invalid selector": { ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"ThisSelectorFailsValidation": "ok"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }, } @@ -216,14 +248,18 @@ func TestServiceRegistryExternalService(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, CreateExternalLoadBalancer: true, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } - storage.Create(ctx, svc) + if _, err := storage.Create(ctx, svc); err != nil { + t.Fatalf("Unexpected error: %v", err) + } if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" { t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) } @@ -234,7 +270,7 @@ func TestServiceRegistryExternalService(t *testing.T) { if srv == nil { t.Errorf("Failed to find service: %s", svc.Name) } - if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Port != 6502 { + if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Ports[0] != 6502 { t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers) } } @@ -245,15 +281,19 @@ func TestServiceRegistryExternalServiceError(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, CreateExternalLoadBalancer: true, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() - storage.Create(ctx, svc) + if _, err := storage.Create(ctx, svc); err == nil { + t.Fatalf("Unexpected success") + } if len(fakeCloud.Calls) != 1 || fakeCloud.Calls[0] != "get-zone" { t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) } @@ -269,8 +309,11 @@ func TestServiceRegistryDelete(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } registry.CreateService(ctx, svc) @@ -291,8 +334,11 @@ func TestServiceRegistryDeleteExternal(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, CreateExternalLoadBalancer: true, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } registry.CreateService(ctx, svc) @@ -313,32 +359,80 @@ func TestServiceRegistryUpdateExternalService(t *testing.T) { svc1 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ - Port: 6502, Selector: map[string]string{"bar": "baz"}, CreateExternalLoadBalancer: false, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } - storage.Create(ctx, svc1) + if _, err := storage.Create(ctx, svc1); err != nil { + t.Fatalf("Unexpected error: %v", err) + } if len(fakeCloud.Calls) != 0 { t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) } // Modify load balancer to be external. - svc2 := new(api.Service) - *svc2 = *svc1 + svc2 := deepCloneService(svc1) svc2.Spec.CreateExternalLoadBalancer = true - storage.Update(ctx, svc2) + if _, _, err := storage.Update(ctx, svc2); err != nil { + t.Fatalf("Unexpected error: %v", err) + } if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" { t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) } // Change port. - svc3 := new(api.Service) - *svc3 = *svc2 - svc3.Spec.Port = 6504 - storage.Update(ctx, svc3) + svc3 := deepCloneService(svc2) + svc3.Spec.Ports[0].Port = 6504 + if _, _, err := storage.Update(ctx, svc3); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(fakeCloud.Calls) != 6 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" || + fakeCloud.Calls[2] != "get-zone" || fakeCloud.Calls[3] != "delete" || + fakeCloud.Calls[4] != "get-zone" || fakeCloud.Calls[5] != "create" { + t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) + } +} + +func TestServiceRegistryUpdateMultiPortExternalService(t *testing.T) { + ctx := api.NewDefaultContext() + storage, _, fakeCloud := NewTestREST(t, nil) + + // Create external load balancer. + svc1 := &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, + Spec: api.ServiceSpec{ + Selector: map[string]string{"bar": "baz"}, + CreateExternalLoadBalancer: true, + SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Name: "p", + Port: 6502, + Protocol: api.ProtocolTCP, + }, { + Name: "q", + Port: 8086, + Protocol: api.ProtocolTCP, + }}, + }, + } + if _, err := storage.Create(ctx, svc1); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" { + t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) + } + + // Modify ports + svc2 := deepCloneService(svc1) + svc2.Spec.Ports[1].Port = 8088 + if _, _, err := storage.Update(ctx, svc2); err != nil { + t.Fatalf("Unexpected error: %v", err) + } if len(fakeCloud.Calls) != 6 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" || fakeCloud.Calls[2] != "get-zone" || fakeCloud.Calls[3] != "delete" || fakeCloud.Calls[4] != "get-zone" || fakeCloud.Calls[5] != "create" { @@ -468,9 +562,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() @@ -487,9 +583,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "bar"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }} ctx = api.NewDefaultContext() created_svc2, _ := rest.Create(ctx, svc2) @@ -506,9 +604,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, PortalIP: "1.2.3.93", - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx = api.NewDefaultContext() @@ -527,9 +627,11 @@ func TestServiceRegistryIPReallocation(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() @@ -548,9 +650,11 @@ func TestServiceRegistryIPReallocation(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "bar"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx = api.NewDefaultContext() @@ -572,33 +676,34 @@ func TestServiceRegistryIPUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() created_svc, _ := rest.Create(ctx, svc) created_service := created_svc.(*api.Service) - if created_service.Spec.Port != 6502 { - t.Errorf("Expected port 6502, but got %v", created_service.Spec.Port) + if created_service.Spec.Ports[0].Port != 6502 { + t.Errorf("Expected port 6502, but got %v", created_service.Spec.Ports[0].Port) } if created_service.Spec.PortalIP != "1.2.3.1" { t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP) } - update := new(api.Service) - *update = *created_service - update.Spec.Port = 6503 + update := deepCloneService(created_service) + update.Spec.Ports[0].Port = 6503 updated_svc, _, _ := rest.Update(ctx, update) updated_service := updated_svc.(*api.Service) - if updated_service.Spec.Port != 6503 { - t.Errorf("Expected port 6503, but got %v", updated_service.Spec.Port) + if updated_service.Spec.Ports[0].Port != 6503 { + t.Errorf("Expected port 6503, but got %v", updated_service.Spec.Ports[0].Port) } - *update = *created_service - update.Spec.Port = 6503 + update = deepCloneService(created_service) + update.Spec.Ports[0].Port = 6503 update.Spec.PortalIP = "1.2.3.76" // error _, _, err := rest.Update(ctx, update) @@ -614,31 +719,32 @@ func TestServiceRegistryIPExternalLoadBalancer(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - Port: 6502, + Selector: map[string]string{"bar": "baz"}, CreateExternalLoadBalancer: true, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() created_svc, _ := rest.Create(ctx, svc) created_service := created_svc.(*api.Service) - if created_service.Spec.Port != 6502 { - t.Errorf("Expected port 6502, but got %v", created_service.Spec.Port) + if created_service.Spec.Ports[0].Port != 6502 { + t.Errorf("Expected port 6502, but got %v", created_service.Spec.Ports[0].Port) } if created_service.Spec.PortalIP != "1.2.3.1" { t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP) } - update := new(api.Service) - *update = *created_service + update := deepCloneService(created_service) _, _, err := rest.Update(ctx, update) if err != nil { t.Errorf("Unexpected error %v", err) } - if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Port != 6502 { + if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Ports[0] != 6502 { t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers) } } @@ -656,9 +762,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } ctx := api.NewDefaultContext() @@ -667,9 +775,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } rest1.Create(ctx, svc) @@ -683,9 +793,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: api.ProtocolTCP, SessionAffinity: api.AffinityTypeNone, + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, } created_svc, _ := rest2.Create(ctx, svc) @@ -743,9 +855,11 @@ func TestCreate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, PortalIP: "None", - Port: 6502, - Protocol: "TCP", SessionAffinity: "None", + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }, // invalid @@ -756,10 +870,12 @@ func TestCreate(t *testing.T) { &api.Service{ Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, - Port: 6502, - Protocol: "TCP", PortalIP: "invalid", SessionAffinity: "None", + Ports: []api.ServicePort{{ + Port: 6502, + Protocol: api.ProtocolTCP, + }}, }, }, ) diff --git a/pkg/service/endpoints_controller.go b/pkg/service/endpoints_controller.go index 2a12148342d..cea75dd4697 100644 --- a/pkg/service/endpoints_controller.go +++ b/pkg/service/endpoints_controller.go @@ -73,44 +73,49 @@ func (e *EndpointController) SyncServiceEndpoints() error { for i := range pods.Items { pod := &pods.Items[i] - // TODO: Once v1beta1 and v1beta2 are EOL'ed, this can - // assume that service.Spec.TargetPort is populated. - _ = v1beta1.Dependency - _ = v1beta2.Dependency - // TODO: Add multiple-ports to Service and expose them here. - portName := "" - portProto := service.Spec.Protocol - portNum, err := findPort(pod, service) - if err != nil { - glog.Errorf("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err) - continue - } - if len(pod.Status.PodIP) == 0 { - glog.Errorf("Failed to find an IP for pod %s/%s", pod.Namespace, pod.Name) - continue - } + for i := range service.Spec.Ports { + servicePort := &service.Spec.Ports[i] - inService := false - for _, c := range pod.Status.Conditions { - if c.Type == api.PodReady && c.Status == api.ConditionTrue { - inService = true - break + // TODO: Once v1beta1 and v1beta2 are EOL'ed, + // this can safely assume that TargetPort is + // populated, and findPort() can be removed. + _ = v1beta1.Dependency + _ = v1beta2.Dependency + + portName := servicePort.Name + portProto := servicePort.Protocol + portNum, err := findPort(pod, servicePort) + if err != nil { + glog.Errorf("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err) + continue + } + if len(pod.Status.PodIP) == 0 { + glog.Errorf("Failed to find an IP for pod %s/%s", pod.Namespace, pod.Name) + continue } - } - if !inService { - glog.V(5).Infof("Pod is out of service: %v/%v", pod.Namespace, pod.Name) - continue - } - epp := api.EndpointPort{Name: portName, Port: portNum, Protocol: portProto} - epa := api.EndpointAddress{IP: pod.Status.PodIP, TargetRef: &api.ObjectReference{ - Kind: "Pod", - Namespace: pod.ObjectMeta.Namespace, - Name: pod.ObjectMeta.Name, - UID: pod.ObjectMeta.UID, - ResourceVersion: pod.ObjectMeta.ResourceVersion, - }} - subsets = append(subsets, api.EndpointSubset{Addresses: []api.EndpointAddress{epa}, Ports: []api.EndpointPort{epp}}) + inService := false + for _, c := range pod.Status.Conditions { + if c.Type == api.PodReady && c.Status == api.ConditionTrue { + inService = true + break + } + } + if !inService { + glog.V(5).Infof("Pod is out of service: %v/%v", pod.Namespace, pod.Name) + continue + } + + epp := api.EndpointPort{Name: portName, Port: portNum, Protocol: portProto} + epa := api.EndpointAddress{IP: pod.Status.PodIP, TargetRef: &api.ObjectReference{ + Kind: "Pod", + Namespace: pod.ObjectMeta.Namespace, + Name: pod.ObjectMeta.Name, + UID: pod.ObjectMeta.UID, + ResourceVersion: pod.ObjectMeta.ResourceVersion, + }} + subsets = append(subsets, api.EndpointSubset{Addresses: []api.EndpointAddress{epa}, Ports: []api.EndpointPort{epp}}) + } } subsets = endpoints.RepackSubsets(subsets) @@ -169,24 +174,24 @@ func findDefaultPort(pod *api.Pod, servicePort int, proto api.Protocol) int { // the service's port. If the targetPort is a non-empty string, look that // string up in all named ports in all containers in the target pod. If no // match is found, fail. -func findPort(pod *api.Pod, service *api.Service) (int, error) { - portName := service.Spec.TargetPort +func findPort(pod *api.Pod, svcPort *api.ServicePort) (int, error) { + portName := svcPort.TargetPort switch portName.Kind { case util.IntstrString: if len(portName.StrVal) == 0 { - return findDefaultPort(pod, service.Spec.Port, service.Spec.Protocol), nil + return findDefaultPort(pod, svcPort.Port, svcPort.Protocol), nil } name := portName.StrVal for _, container := range pod.Spec.Containers { for _, port := range container.Ports { - if port.Name == name && port.Protocol == service.Spec.Protocol { + if port.Name == name && port.Protocol == svcPort.Protocol { return port.ContainerPort, nil } } } case util.IntstrInt: if portName.IntVal == 0 { - return findDefaultPort(pod, service.Spec.Port, service.Spec.Protocol), nil + return findDefaultPort(pod, svcPort.Port, svcPort.Protocol), nil } return portName.IntVal, nil } diff --git a/pkg/service/endpoints_controller_test.go b/pkg/service/endpoints_controller_test.go index 70900e7d926..59de0b591be 100644 --- a/pkg/service/endpoints_controller_test.go +++ b/pkg/service/endpoints_controller_test.go @@ -51,7 +51,8 @@ func newPodList(nPods int, nPorts int) *api.PodList { }, } for j := 0; j < nPorts; j++ { - p.Spec.Containers[0].Ports = append(p.Spec.Containers[0].Ports, api.ContainerPort{ContainerPort: 8080 + j}) + p.Spec.Containers[0].Ports = append(p.Spec.Containers[0].Ports, + api.ContainerPort{Name: fmt.Sprintf("port%d", i), ContainerPort: 8080 + j}) } pods = append(pods, p) } @@ -203,7 +204,7 @@ func TestFindPort(t *testing.T) { for _, tc := range testCases { port, err := findPort(&api.Pod{Spec: api.PodSpec{Containers: tc.containers}}, - &api.Service{Spec: api.ServiceSpec{Protocol: "TCP", Port: servicePort, TargetPort: tc.port}}) + &api.ServicePort{Protocol: "TCP", Port: servicePort, TargetPort: tc.port}) if err != nil && tc.pass { t.Errorf("unexpected error for %s: %v", tc.name, err) } @@ -277,7 +278,7 @@ func TestSyncEndpointsItemsPreserveNoSelector(t *testing.T) { Items: []api.Service{ { ObjectMeta: api.ObjectMeta{Name: "foo"}, - Spec: api.ServiceSpec{}, + Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 80}}}, }, }, } @@ -310,7 +311,7 @@ func TestSyncEndpointsProtocolTCP(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ Selector: map[string]string{}, - Protocol: api.ProtocolTCP, + Ports: []api.ServicePort{{Port: 80}}, }, }, }, @@ -344,7 +345,7 @@ func TestSyncEndpointsProtocolUDP(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ Selector: map[string]string{}, - Protocol: api.ProtocolUDP, + Ports: []api.ServicePort{{Port: 80}}, }, }, }, @@ -378,6 +379,7 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ Selector: map[string]string{}, + Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}}, }, }, }, @@ -417,9 +419,8 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: api.ServiceSpec{ - Selector: map[string]string{ - "foo": "bar", - }, + Selector: map[string]string{"foo": "bar"}, + Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}}, }, }, }, @@ -462,9 +463,8 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Selector: map[string]string{ - "foo": "bar", - }, + Selector: map[string]string{"foo": "bar"}, + Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}}, }, }, }, @@ -496,8 +496,10 @@ func TestSyncEndpointsItems(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, Spec: api.ServiceSpec{ - Selector: map[string]string{ - "foo": "bar", + Selector: map[string]string{"foo": "bar"}, + Ports: []api.ServicePort{ + {Name: "port0", Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}, + {Name: "port1", Port: 88, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8088)}, }, }, }, @@ -520,7 +522,8 @@ func TestSyncEndpointsItems(t *testing.T) { {IP: "1.2.3.6", TargetRef: &api.ObjectReference{Kind: "Pod", Name: "pod2"}}, }, Ports: []api.EndpointPort{ - {Port: 8080, Protocol: "TCP"}, + {Name: "port0", Port: 8080, Protocol: "TCP"}, + {Name: "port1", Port: 8088, Protocol: "TCP"}, }, }} data := runtime.EncodeOrDie(testapi.Codec(), &api.Endpoints{ @@ -540,9 +543,8 @@ func TestSyncEndpointsPodError(t *testing.T) { { ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, Spec: api.ServiceSpec{ - Selector: map[string]string{ - "foo": "bar", - }, + Selector: map[string]string{"foo": "bar"}, + Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}}, }, }, }, diff --git a/pkg/types/testing.go b/pkg/types/testing.go deleted file mode 100644 index 1a8c87c529c..00000000000 --- a/pkg/types/testing.go +++ /dev/null @@ -1,26 +0,0 @@ -/* -Copyright 2015 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package types - -import "fmt" - -func NewNamespacedNameOrDie(namespace, name string) (ret NamespacedName) { - if len(namespace) == 0 || len(name) == 0 { - panic(fmt.Sprintf("invalid call to NewNamespacedNameOrDie(%q, %q)", namespace, name)) - } - return NamespacedName{namespace, name} -} diff --git a/pkg/util/hash_test.go b/pkg/util/hash_test.go index db766811cec..4397dfd9ec7 100644 --- a/pkg/util/hash_test.go +++ b/pkg/util/hash_test.go @@ -17,10 +17,106 @@ limitations under the License. package util import ( + "fmt" "hash/adler32" "testing" + + "github.com/davecgh/go-spew/spew" ) +type A struct { + x int + y string +} + +type B struct { + x []int + y map[string]bool +} + +type C struct { + x int + y string +} + +func (c C) String() string { + return fmt.Sprintf("%d:%s", c.x, c.y) +} + +func TestDeepHashObject(t *testing.T) { + // These types are known to fail object hashing because of how spew implements map keys. + // http://github.com/davecgh/go-spew/issues/30 + failureCases := []func() interface{}{ + func() interface{} { return map[A]bool{A{8675309, "Jenny"}: true, A{9765683, "!Jenny"}: false} }, + func() interface{} { return map[C]bool{C{8675309, "Jenny"}: true, C{9765683, "!Jenny"}: false} }, + func() interface{} { return map[*A]bool{&A{8675309, "Jenny"}: true, &A{9765683, "!Jenny"}: false} }, + func() interface{} { return map[*C]bool{&C{8675309, "Jenny"}: true, &C{9765683, "!Jenny"}: false} }, + } + for _, tc := range failureCases { + hasher := adler32.New() + DeepHashObject(hasher, tc()) + first := hasher.Sum32() + alwaysSame := false + for i := 0; i < 100; i++ { + DeepHashObject(hasher, tc()) + if hasher.Sum32() != first { + alwaysSame = false + break + } + } + if alwaysSame { + t.Errorf("expected failure for %q", toString(tc())) + } + } + + successCases := []func() interface{}{ + func() interface{} { return 8675309 }, + func() interface{} { return "Jenny, I got your number" }, + func() interface{} { return []string{"eight", "six", "seven"} }, + func() interface{} { return [...]int{5, 3, 0, 9} }, + func() interface{} { return map[int]string{8: "8", 6: "6", 7: "7"} }, + func() interface{} { return map[string]int{"5": 5, "3": 3, "0": 0, "9": 9} }, + func() interface{} { return A{867, "5309"} }, + func() interface{} { return &A{867, "5309"} }, + func() interface{} { + return B{[]int{8, 6, 7}, map[string]bool{"5": true, "3": true, "0": true, "9": true}} + }, + } + + for _, tc := range successCases { + hasher1 := adler32.New() + DeepHashObject(hasher1, tc()) + hash1 := hasher1.Sum32() + DeepHashObject(hasher1, tc()) + hash2 := hasher1.Sum32() + if hash1 != hash2 { + t.Fatalf("hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash1, hash2) + } + for i := 0; i < 100; i++ { + hasher2 := adler32.New() + + DeepHashObject(hasher1, tc()) + hash1a := hasher1.Sum32() + DeepHashObject(hasher2, tc()) + hash2a := hasher2.Sum32() + + if hash1a != hash1 { + t.Errorf("repeated hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash1, hash1a) + } + if hash2a != hash2 { + t.Errorf("repeated hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash2, hash2a) + } + if hash1a != hash2a { + t.Errorf("hash of the same object produced (%q) different results: %d vs %d", toString(tc()), hash1a, hash2a) + } + } + } +} + +func toString(obj interface{}) string { + return spew.Sprintf("%#v", obj) +} + type wheel struct { radius uint32 } diff --git a/test/e2e/networking.go b/test/e2e/networking.go index 5a5af6c781c..7a2afd34616 100644 --- a/test/e2e/networking.go +++ b/test/e2e/networking.go @@ -77,8 +77,11 @@ var _ = Describe("Networking", func() { }, }, Spec: api.ServiceSpec{ - Port: 8080, - TargetPort: util.NewIntOrStringFromInt(8080), + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 8080, + TargetPort: util.NewIntOrStringFromInt(8080), + }}, Selector: map[string]string{ "name": name, }, diff --git a/test/e2e/pods.go b/test/e2e/pods.go index 9d16a950bad..7e9812dff4e 100644 --- a/test/e2e/pods.go +++ b/test/e2e/pods.go @@ -307,8 +307,10 @@ var _ = Describe("Pods", func() { }, }, Spec: api.ServiceSpec{ - Port: 8765, - TargetPort: util.NewIntOrStringFromInt(8080), + Ports: []api.ServicePort{{ + Port: 8765, + TargetPort: util.NewIntOrStringFromInt(8080), + }}, Selector: map[string]string{ "name": serverName, }, diff --git a/test/e2e/service.go b/test/e2e/service.go index c93925d1399..661e0dd820b 100644 --- a/test/e2e/service.go +++ b/test/e2e/service.go @@ -204,9 +204,11 @@ var _ = Describe("Services", func() { Name: serviceName, }, Spec: api.ServiceSpec{ - Port: 80, - Selector: labels, - TargetPort: util.NewIntOrStringFromInt(80), + Selector: labels, + Ports: []api.ServicePort{{ + Port: 80, + TargetPort: util.NewIntOrStringFromInt(80), + }}, }, } _, err := c.Services(ns).Create(service) @@ -264,9 +266,11 @@ var _ = Describe("Services", func() { Name: serviceName, }, Spec: api.ServiceSpec{ - Port: 80, - Selector: labels, - TargetPort: util.NewIntOrStringFromInt(80), + Selector: labels, + Ports: []api.ServicePort{{ + Port: 80, + TargetPort: util.NewIntOrStringFromInt(80), + }}, CreateExternalLoadBalancer: true, }, } @@ -287,7 +291,7 @@ var _ = Describe("Services", func() { Failf("got unexpected number (%d) of public IPs for externally load balanced service: %v", result.Spec.PublicIPs, result) } ip := result.Spec.PublicIPs[0] - port := result.Spec.Port + port := result.Spec.Ports[0].Port pod := &api.Pod{ TypeMeta: api.TypeMeta{ @@ -351,9 +355,11 @@ var _ = Describe("Services", func() { service := &api.Service{ ObjectMeta: api.ObjectMeta{}, Spec: api.ServiceSpec{ - Port: 80, - Selector: labels, - TargetPort: util.NewIntOrStringFromInt(80), + Selector: labels, + Ports: []api.ServicePort{{ + Port: 80, + TargetPort: util.NewIntOrStringFromInt(80), + }}, CreateExternalLoadBalancer: true, }, } diff --git a/test/soak/serve_hostnames/serve_hostnames.go b/test/soak/serve_hostnames/serve_hostnames.go index 6a44e561f7c..f926fef6685 100644 --- a/test/soak/serve_hostnames/serve_hostnames.go +++ b/test/soak/serve_hostnames/serve_hostnames.go @@ -108,8 +108,11 @@ func main() { }, }, Spec: api.ServiceSpec{ - Port: 9376, - TargetPort: util.NewIntOrStringFromInt(9376), + Ports: []api.ServicePort{{ + Protocol: "TCP", + Port: 9376, + TargetPort: util.NewIntOrStringFromInt(9376), + }}, Selector: map[string]string{ "name": "serve-hostname", },