diff --git a/api/swagger-spec/v1beta1.json b/api/swagger-spec/v1beta1.json index cef4299e613..08c608ec158 100644 --- a/api/swagger-spec/v1beta1.json +++ b/api/swagger-spec/v1beta1.json @@ -4742,21 +4742,40 @@ } ] }, + { + "path": "/api/v1beta1/pods/{name}/exec", + "description": "API at /api/v1beta1 version v1beta1", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta1/pods/{name}/log", "description": "API at /api/v1beta1 version v1beta1", "operations": [ { - "type": "v1beta1.PodLogOptions", + "type": "v1beta1.Pod", "method": "GET", - "summary": "read the specified PodLogOptions", - "nickname": "readPodLogOptions", + "summary": "read the specified Pod", + "nickname": "readPod", "parameters": [ { "type": "string", "paramType": "path", "name": "name", - "description": "name of the PodLogOptions", + "description": "name of the Pod", "required": true, "allowMultiple": false }, @@ -4773,7 +4792,7 @@ { "code": 200, "message": "OK", - "responseModel": "v1beta1.PodLogOptions" + "responseModel": "v1beta1.Pod" } ], "produces": [ @@ -4785,6 +4804,193 @@ } ] }, + { + "path": "/api/v1beta1/pods/{name}/portforward", + "description": "API at /api/v1beta1 version v1beta1", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta1/pods/{name}/proxy", + "description": "API at /api/v1beta1 version v1beta1", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta1/pods/{name}/proxy/{path:*}", + "description": "API at /api/v1beta1 version v1beta1", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta1/pods/{name}/status", "description": "API at /api/v1beta1 version v1beta1", @@ -6914,6 +7120,31 @@ "id": "", "properties": null }, + "v1beta1.AWSElasticBlockStoreVolumeSource": { + "id": "v1beta1.AWSElasticBlockStoreVolumeSource", + "required": [ + "volumeID" + ], + "properties": { + "fsType": { + "type": "string", + "description": "file system type to mount, such as ext4, xfs, ntfs" + }, + "partition": { + "type": "integer", + "format": "int32", + "description": "partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted" + }, + "readOnly": { + "type": "boolean", + "description": "read-only if true, read-write otherwise (false or unspecified)" + }, + "volumeID": { + "type": "string", + "description": "unique id of the PD resource in AWS" + } + } + }, "v1beta1.AccessModeType": { "id": "v1beta1.AccessModeType", "properties": {} @@ -8668,6 +8899,19 @@ } } }, + "v1beta1.PersistentVolumeClaimVolumeSource": { + "id": "v1beta1.PersistentVolumeClaimVolumeSource", + "properties": { + "claimName": { + "type": "string", + "description": "the name of the claim in the same namespace to be mounted as a volume" + }, + "readOnly": { + "type": "boolean", + "description": "mount volume as read-only when true; default false" + } + } + }, "v1beta1.PersistentVolumeList": { "id": "v1beta1.PersistentVolumeList", "properties": { @@ -8727,9 +8971,10 @@ "v1beta1.PersistentVolumeSpec": { "id": "v1beta1.PersistentVolumeSpec", "required": [ - "persistentDisk", "hostPath", - "glusterfs" + "glusterfs", + "persistentDisk", + "awsElasticBlockStore" ], "properties": { "accessModes": { @@ -8739,6 +8984,10 @@ }, "description": "all ways the volume can be mounted" }, + "awsElasticBlockStore": { + "$ref": "v1beta1.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource provisioned by an admin" + }, "capacity": { "type": "any", "description": "a description of the persistent volume's resources and capacity" @@ -8911,63 +9160,6 @@ } } }, - "v1beta1.PodLogOptions": { - "id": "v1beta1.PodLogOptions", - "properties": { - "annotations": { - "type": "any", - "description": "map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about the object" - }, - "apiVersion": { - "type": "string", - "description": "version of the schema the object should have" - }, - "container": { - "type": "string", - "description": "the container for which to stream logs; defaults to only container if there is one container in the pod" - }, - "creationTimestamp": { - "type": "string", - "description": "RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists" - }, - "deletionTimestamp": { - "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" - }, - "follow": { - "type": "boolean", - "description": "follow the log stream of the pod; defaults to false" - }, - "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" - }, - "id": { - "type": "string", - "description": "name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs; cannot be updated" - }, - "kind": { - "type": "string", - "description": "kind of object, in CamelCase; cannot be updated" - }, - "namespace": { - "type": "string", - "description": "namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated" - }, - "resourceVersion": { - "$ref": "uint64", - "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" - }, - "selfLink": { - "type": "string", - "description": "URL for the object; populated by the system, read-only" - }, - "uid": { - "type": "string", - "description": "unique UUID across space and time; populated by the system, read-only; cannot be updated" - } - } - }, "v1beta1.PodState": { "id": "v1beta1.PodState", "properties": { @@ -9812,7 +10004,7 @@ }, "source": { "$ref": "v1beta1.VolumeSource", - "description": "location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, or GitRepo; default is EmptyDir" + "description": "location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, AWSElasticBlockStore, or GitRepo; default is EmptyDir" } } }, @@ -9851,6 +10043,7 @@ "hostDir", "emptyDir", "persistentDisk", + "awsElasticBlockStore", "gitRepo", "secret", "nfs", @@ -9858,6 +10051,10 @@ "glusterfs" ], "properties": { + "awsElasticBlockStore": { + "$ref": "v1beta1.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource attached to the host machine on demand" + }, "emptyDir": { "$ref": "v1beta1.EmptyDirVolumeSource", "description": "temporary directory that shares a pod's lifetime" @@ -9886,6 +10083,10 @@ "$ref": "v1beta1.GCEPersistentDiskVolumeSource", "description": "GCE disk resource attached to the host machine on demand" }, + "persistentVolumeClaim": { + "$ref": "v1beta1.PersistentVolumeClaimVolumeSource", + "description": "a reference to a PersistentVolumeClaim in the same namespace" + }, "secret": { "$ref": "v1beta1.SecretVolumeSource", "description": "secret to populate volume with" diff --git a/api/swagger-spec/v1beta2.json b/api/swagger-spec/v1beta2.json index 56625e84621..58a626423da 100644 --- a/api/swagger-spec/v1beta2.json +++ b/api/swagger-spec/v1beta2.json @@ -4742,21 +4742,40 @@ } ] }, + { + "path": "/api/v1beta2/pods/{name}/exec", + "description": "API at /api/v1beta2 version v1beta2", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta2/pods/{name}/log", "description": "API at /api/v1beta2 version v1beta2", "operations": [ { - "type": "v1beta2.PodLogOptions", + "type": "v1beta2.Pod", "method": "GET", - "summary": "read the specified PodLogOptions", - "nickname": "readPodLogOptions", + "summary": "read the specified Pod", + "nickname": "readPod", "parameters": [ { "type": "string", "paramType": "path", "name": "name", - "description": "name of the PodLogOptions", + "description": "name of the Pod", "required": true, "allowMultiple": false }, @@ -4773,7 +4792,7 @@ { "code": 200, "message": "OK", - "responseModel": "v1beta2.PodLogOptions" + "responseModel": "v1beta2.Pod" } ], "produces": [ @@ -4785,6 +4804,193 @@ } ] }, + { + "path": "/api/v1beta2/pods/{name}/portforward", + "description": "API at /api/v1beta2 version v1beta2", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta2/pods/{name}/proxy", + "description": "API at /api/v1beta2 version v1beta2", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta2/pods/{name}/proxy/{path:*}", + "description": "API at /api/v1beta2 version v1beta2", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta2/pods/{name}/status", "description": "API at /api/v1beta2 version v1beta2", @@ -6914,6 +7120,31 @@ "id": "", "properties": null }, + "v1beta2.AWSElasticBlockStoreVolumeSource": { + "id": "v1beta2.AWSElasticBlockStoreVolumeSource", + "required": [ + "volumeID" + ], + "properties": { + "fsType": { + "type": "string", + "description": "file system type to mount, such as ext4, xfs, ntfs" + }, + "partition": { + "type": "integer", + "format": "int32", + "description": "partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted" + }, + "readOnly": { + "type": "boolean", + "description": "read-only if true, read-write otherwise (false or unspecified)" + }, + "volumeID": { + "type": "string", + "description": "unique id of the PD resource in AWS" + } + } + }, "v1beta2.AccessModeType": { "id": "v1beta2.AccessModeType", "properties": {} @@ -8657,6 +8888,19 @@ } } }, + "v1beta2.PersistentVolumeClaimVolumeSource": { + "id": "v1beta2.PersistentVolumeClaimVolumeSource", + "properties": { + "claimName": { + "type": "string", + "description": "the name of the claim in the same namespace to be mounted as a volume" + }, + "readOnly": { + "type": "boolean", + "description": "mount volume as read-only when true; default false" + } + } + }, "v1beta2.PersistentVolumeList": { "id": "v1beta2.PersistentVolumeList", "properties": { @@ -8717,6 +8961,7 @@ "id": "v1beta2.PersistentVolumeSpec", "required": [ "persistentDisk", + "awsElasticBlockStore", "hostPath", "glusterfs" ], @@ -8728,6 +8973,10 @@ }, "description": "all ways the volume can be mounted" }, + "awsElasticBlockStore": { + "$ref": "v1beta2.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource provisioned by an admin" + }, "capacity": { "type": "any", "description": "a description of the persistent volume's resources and capacity" @@ -8900,63 +9149,6 @@ } } }, - "v1beta2.PodLogOptions": { - "id": "v1beta2.PodLogOptions", - "properties": { - "annotations": { - "type": "any", - "description": "map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about the object" - }, - "apiVersion": { - "type": "string", - "description": "version of the schema the object should have" - }, - "container": { - "type": "string", - "description": "the container for which to stream logs; defaults to only container if there is one container in the pod" - }, - "creationTimestamp": { - "type": "string", - "description": "RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists" - }, - "deletionTimestamp": { - "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" - }, - "follow": { - "type": "boolean", - "description": "follow the log stream of the pod; defaults to false" - }, - "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" - }, - "id": { - "type": "string", - "description": "name of the object; must be a DNS_SUBDOMAIN and unique among all objects of the same kind within the same namespace; used in resource URLs; cannot be updated" - }, - "kind": { - "type": "string", - "description": "kind of object, in CamelCase; cannot be updated" - }, - "namespace": { - "type": "string", - "description": "namespace to which the object belongs; must be a DNS_SUBDOMAIN; 'default' by default; cannot be updated" - }, - "resourceVersion": { - "$ref": "uint64", - "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" - }, - "selfLink": { - "type": "string", - "description": "URL for the object; populated by the system, read-only" - }, - "uid": { - "type": "string", - "description": "unique UUID across space and time; populated by the system, read-only" - } - } - }, "v1beta2.PodState": { "id": "v1beta2.PodState", "properties": { @@ -9801,7 +9993,7 @@ }, "source": { "$ref": "v1beta2.VolumeSource", - "description": "location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, or GitRepo; default is EmptyDir" + "description": "location and type of volume to mount; at most one of HostDir, EmptyDir, GCEPersistentDisk, AWSElasticBlockStore, or GitRepo; default is EmptyDir" } } }, @@ -9832,6 +10024,7 @@ "hostDir", "emptyDir", "persistentDisk", + "awsElasticBlockStore", "gitRepo", "secret", "nfs", @@ -9839,6 +10032,10 @@ "glusterfs" ], "properties": { + "awsElasticBlockStore": { + "$ref": "v1beta2.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource attached to the host machine on demand" + }, "emptyDir": { "$ref": "v1beta2.EmptyDirVolumeSource", "description": "temporary directory that shares a pod's lifetime" @@ -9867,6 +10064,10 @@ "$ref": "v1beta2.GCEPersistentDiskVolumeSource", "description": "GCE disk resource attached to the host machine on demand" }, + "persistentVolumeClaim": { + "$ref": "v1beta2.PersistentVolumeClaimVolumeSource", + "description": "a reference to a PersistentVolumeClaim in the same namespace" + }, "secret": { "$ref": "v1beta2.SecretVolumeSource", "description": "secret to populate volume" diff --git a/api/swagger-spec/v1beta3.json b/api/swagger-spec/v1beta3.json index b989fcf0a0c..332703f42cf 100644 --- a/api/swagger-spec/v1beta3.json +++ b/api/swagger-spec/v1beta3.json @@ -4722,21 +4722,40 @@ } ] }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/exec", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/log", "description": "API at /api/v1beta3 version v1beta3", "operations": [ { - "type": "v1beta3.PodLogOptions", + "type": "v1beta3.Pod", "method": "GET", - "summary": "read the specified PodLogOptions", - "nickname": "readPodLogOptions", + "summary": "read the specified Pod", + "nickname": "readPod", "parameters": [ { "type": "string", "paramType": "path", "name": "name", - "description": "name of the PodLogOptions", + "description": "name of the Pod", "required": true, "allowMultiple": false }, @@ -4753,7 +4772,7 @@ { "code": 200, "message": "OK", - "responseModel": "v1beta3.PodLogOptions" + "responseModel": "v1beta3.Pod" } ], "produces": [ @@ -4765,6 +4784,193 @@ } ] }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/portforward", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/proxy", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/proxy/{path:*}", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "string", + "method": "GET", + "summary": "connect GET requests to Pod", + "nickname": "connectGETPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "POST", + "summary": "connect POST requests to Pod", + "nickname": "connectPOSTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "PUT", + "summary": "connect PUT requests to Pod", + "nickname": "connectPUTPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "DELETE", + "summary": "connect DELETE requests to Pod", + "nickname": "connectDELETEPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "HEAD", + "summary": "connect HEAD requests to Pod", + "nickname": "connectHEADPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "string", + "method": "OPTIONS", + "summary": "connect OPTIONS requests to Pod", + "nickname": "connectOPTIONSPod", + "parameters": [], + "produces": [ + "*/*" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta3/namespaces/{namespaces}/pods/{name}/status", "description": "API at /api/v1beta3 version v1beta3", @@ -4816,6 +5022,550 @@ } ] }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/podtemplates", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "v1beta3.PodTemplateList", + "method": "GET", + "summary": "list or watch objects of kind PodTemplate", + "nickname": "listPodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "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": "boolean", + "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 + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.PodTemplateList" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "v1beta3.PodTemplate", + "method": "POST", + "summary": "create a PodTemplate", + "nickname": "createPodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "v1beta3.PodTemplate", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.PodTemplate" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/watch/namespaces/{namespaces}/podtemplates", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "json.WatchEvent", + "method": "GET", + "summary": "watch individual changes to a list of PodTemplate", + "nickname": "watchPodTemplatelist", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "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": "boolean", + "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 + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "json.WatchEvent" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/namespaces/{namespaces}/podtemplates/{name}", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "v1beta3.PodTemplate", + "method": "GET", + "summary": "read the specified PodTemplate", + "nickname": "readPodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the PodTemplate", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.PodTemplate" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "v1beta3.PodTemplate", + "method": "PUT", + "summary": "replace the specified PodTemplate", + "nickname": "replacePodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the PodTemplate", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "v1beta3.PodTemplate", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.PodTemplate" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + }, + { + "type": "v1beta3.PodTemplate", + "method": "PATCH", + "summary": "partially update the specified PodTemplate", + "nickname": "patchPodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the PodTemplate", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "string" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json-patch+json", + "application/merge-patch+json", + "application/strategic-merge-patch+json" + ] + }, + { + "type": "v1beta3.Status", + "method": "DELETE", + "summary": "delete a PodTemplate", + "nickname": "deletePodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the PodTemplate", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "v1beta3.DeleteOptions", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.Status" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/watch/namespaces/{namespaces}/podtemplates/{name}", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "json.WatchEvent", + "method": "GET", + "summary": "watch changes to an object of kind PodTemplate", + "nickname": "watchPodTemplate", + "parameters": [ + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the PodTemplate", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespaces", + "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": "boolean", + "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 + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "json.WatchEvent" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/podtemplates", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "v1beta3.PodTemplateList", + "method": "GET", + "summary": "list or watch objects of kind PodTemplate", + "nickname": "listPodTemplate", + "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": "boolean", + "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 + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1beta3.PodTemplateList" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, + { + "path": "/api/v1beta3/watch/podtemplates", + "description": "API at /api/v1beta3 version v1beta3", + "operations": [ + { + "type": "json.WatchEvent", + "method": "GET", + "summary": "watch individual changes to a list of PodTemplate", + "nickname": "watchPodTemplatelist", + "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": "boolean", + "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 + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "json.WatchEvent" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1beta3/namespaces/{namespaces}/replicationcontrollers", "description": "API at /api/v1beta3 version v1beta3", @@ -7362,6 +8112,31 @@ } } }, + "v1beta3.AWSElasticBlockStoreVolumeSource": { + "id": "v1beta3.AWSElasticBlockStoreVolumeSource", + "required": [ + "volumeID" + ], + "properties": { + "fsType": { + "type": "string", + "description": "file system type to mount, such as ext4, xfs, ntfs" + }, + "partition": { + "type": "integer", + "format": "int32", + "description": "partition on the disk to mount (e.g., '1' for /dev/sda1); if omitted the plain device name (e.g., /dev/sda) will be mounted" + }, + "readOnly": { + "type": "boolean", + "description": "read-only if true, read-write otherwise (false or unspecified)" + }, + "volumeID": { + "type": "string", + "description": "unique id of the PD resource in AWS" + } + } + }, "v1beta3.AccessModeType": { "id": "v1beta3.AccessModeType", "properties": {} @@ -8885,6 +9660,19 @@ } } }, + "v1beta3.PersistentVolumeClaimVolumeSource": { + "id": "v1beta3.PersistentVolumeClaimVolumeSource", + "properties": { + "claimName": { + "type": "string", + "description": "the name of the claim in the same namespace to be mounted as a volume" + }, + "readOnly": { + "type": "boolean", + "description": "mount volume as read-only when true; default false" + } + } + }, "v1beta3.PersistentVolumeList": { "id": "v1beta3.PersistentVolumeList", "properties": { @@ -8917,6 +9705,7 @@ "id": "v1beta3.PersistentVolumeSpec", "required": [ "gcePersistentDisk", + "awsElasticBlockStore", "hostPath", "glusterfs" ], @@ -8928,6 +9717,10 @@ }, "description": "all ways the volume can be mounted" }, + "awsElasticBlockStore": { + "$ref": "v1beta3.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource provisioned by an admin" + }, "capacity": { "type": "any", "description": "a description of the persistent volume's resources and capacity" @@ -9068,27 +9861,6 @@ } } }, - "v1beta3.PodLogOptions": { - "id": "v1beta3.PodLogOptions", - "properties": { - "apiVersion": { - "type": "string", - "description": "version of the schema the object should have" - }, - "container": { - "type": "string", - "description": "the container for which to stream logs; defaults to only container if there is one container in the pod" - }, - "follow": { - "type": "boolean", - "description": "follow the log stream of the pod; defaults to false" - }, - "kind": { - "type": "string", - "description": "kind of object, in CamelCase; cannot be updated" - } - } - }, "v1beta3.PodSpec": { "id": "v1beta3.PodSpec", "required": [ @@ -9167,6 +9939,94 @@ } } }, + "v1beta3.PodTemplate": { + "id": "v1beta3.PodTemplate", + "properties": { + "annotations": { + "type": "any", + "description": "map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about objects" + }, + "apiVersion": { + "type": "string", + "description": "version of the schema the object should have" + }, + "creationTimestamp": { + "type": "string", + "description": "RFC 3339 date and time at which the object was created; populated by the system, read-only; null for lists" + }, + "deletionTimestamp": { + "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" + }, + "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" + }, + "kind": { + "type": "string", + "description": "kind of object, in CamelCase; cannot be updated" + }, + "labels": { + "type": "any", + "description": "map of string keys and values that can be used to organize and categorize objects; may match selectors of replication controllers and services" + }, + "name": { + "type": "string", + "description": "string that identifies an object. Must be unique within a namespace; cannot be updated" + }, + "namespace": { + "type": "string", + "description": "namespace of the object; cannot be updated" + }, + "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" + }, + "selfLink": { + "type": "string", + "description": "URL for the object; populated by the system, read-only" + }, + "template": { + "$ref": "v1beta3.PodTemplateSpec", + "description": "the template of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status" + }, + "uid": { + "type": "string", + "description": "unique UUID across space and time; populated by the system; read-only" + } + } + }, + "v1beta3.PodTemplateList": { + "id": "v1beta3.PodTemplateList", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "type": "string", + "description": "version of the schema the object should have" + }, + "items": { + "type": "array", + "items": { + "$ref": "v1beta3.PodTemplate" + }, + "description": "list of pod templates" + }, + "kind": { + "type": "string", + "description": "kind of object, in CamelCase; cannot be updated" + }, + "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" + }, + "selfLink": { + "type": "string", + "description": "URL for the object; populated by the system, read-only" + } + } + }, "v1beta3.PodTemplateSpec": { "id": "v1beta3.PodTemplateSpec", "properties": { @@ -9348,7 +10208,7 @@ }, "selector": { "type": "any", - "description": "label keys and values that must match in order to be controlled by this replication controller" + "description": "label keys and values that must match in order to be controlled by this replication controller, if empty defaulted to labels on Pod template" }, "template": { "$ref": "v1beta3.PodTemplateSpec", @@ -9864,16 +10724,21 @@ "id": "v1beta3.Volume", "required": [ "name", - "gcePersistentDisk", - "gitRepo", + "emptyDir", "secret", - "nfs", - "iscsi", "glusterfs", "hostPath", - "emptyDir" + "awsElasticBlockStore", + "gitRepo", + "nfs", + "iscsi", + "gcePersistentDisk" ], "properties": { + "awsElasticBlockStore": { + "$ref": "v1beta3.AWSElasticBlockStoreVolumeSource", + "description": "AWS disk resource attached to the host machine on demand" + }, "emptyDir": { "$ref": "v1beta3.EmptyDirVolumeSource", "description": "temporary directory that shares a pod's lifetime" @@ -9906,6 +10771,10 @@ "$ref": "v1beta3.NFSVolumeSource", "description": "NFS volume that will be mounted in the host machine" }, + "persistentVolumeClaim": { + "$ref": "v1beta3.PersistentVolumeClaimVolumeSource", + "description": "a reference to a PersistentVolumeClaim in the same namespace" + }, "secret": { "$ref": "v1beta3.SecretVolumeSource", "description": "secret to populate volume" diff --git a/cluster/addons/dns/kube2sky/kube2sky.go b/cluster/addons/dns/kube2sky/kube2sky.go index e8d9aeac824..fb940136c56 100644 --- a/cluster/addons/dns/kube2sky/kube2sky.go +++ b/cluster/addons/dns/kube2sky/kube2sky.go @@ -29,6 +29,7 @@ import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + kclientcmd "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" kfields "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" klabels "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" tools "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" @@ -42,6 +43,7 @@ var ( etcd_mutation_timeout = flag.Duration("etcd_mutation_timeout", 10*time.Second, "crash after retrying etcd mutation for a specified duration") etcd_server = flag.String("etcd-server", "http://127.0.0.1:4001", "URL to etcd server") verbose = flag.Bool("verbose", false, "log extra information") + kubecfg_file = flag.String("kubecfg_file", "", "Location of kubecfg file for access to kubernetes service") ) func removeDNS(record string, etcdClient *etcd.Client) error { @@ -128,22 +130,31 @@ func newEtcdClient() (client *etcd.Client) { // TODO: evaluate using pkg/client/clientcmd func newKubeClient() (*kclient.Client, error) { - config := &kclient.Config{} - - masterHost := os.Getenv("KUBERNETES_RO_SERVICE_HOST") - if masterHost == "" { - log.Fatalf("KUBERNETES_RO_SERVICE_HOST is not defined") + var config *kclient.Config + if *kubecfg_file == "" { + // No kubecfg file provided. Use kubernetes_ro service. + masterHost := os.Getenv("KUBERNETES_RO_SERVICE_HOST") + if masterHost == "" { + log.Fatalf("KUBERNETES_RO_SERVICE_HOST is not defined") + } + masterPort := os.Getenv("KUBERNETES_RO_SERVICE_PORT") + if masterPort == "" { + log.Fatalf("KUBERNETES_RO_SERVICE_PORT is not defined") + } + config = &kclient.Config{ + Host: fmt.Sprintf("http://%s:%s", masterHost, masterPort), + Version: "v1beta1", + } + } else { + var err error + if config, err = kclientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &kclientcmd.ClientConfigLoadingRules{ExplicitPath: *kubecfg_file}, + &kclientcmd.ConfigOverrides{}).ClientConfig(); err != nil { + return nil, err + } } - masterPort := os.Getenv("KUBERNETES_RO_SERVICE_PORT") - if masterPort == "" { - log.Fatalf("KUBERNETES_RO_SERVICE_PORT is not defined") - } - config.Host = fmt.Sprintf("http://%s:%s", masterHost, masterPort) log.Printf("Using %s for kubernetes master", config.Host) - - config.Version = "v1beta1" log.Printf("Using kubernetes API %s", config.Version) - return kclient.New(config) } diff --git a/cluster/addons/dns/skydns-rc.yaml.in b/cluster/addons/dns/skydns-rc.yaml.in index 048785f9738..7329ef3a3a2 100644 --- a/cluster/addons/dns/skydns-rc.yaml.in +++ b/cluster/addons/dns/skydns-rc.yaml.in @@ -29,10 +29,15 @@ desiredState: "-advertise-client-urls=http://127.0.0.1:4001", ] - name: kube2sky - image: gcr.io/google_containers/kube2sky:1.1 + image: gcr.io/google_containers/kube2sky:1.2 + volumeMounts: + - name: dns-token + mountPath: /etc/dns_token + readOnly: true command: [ # entrypoint = "/kube2sky", "-domain={{ pillar['dns_domain'] }}", + "-kubecfg_file=/etc/dns_token/kubeconfig", ] - name: skydns image: gcr.io/google_containers/skydns:2015-03-11-001 @@ -46,3 +51,11 @@ desiredState: - name: dns containerPort: 53 protocol: UDP + volumes: + - name: dns-token + source: + secret: + target: + kind: Secret + namespace: default + name: token-system-dns \ No newline at end of file diff --git a/cluster/aws/templates/create-dynamic-salt-files.sh b/cluster/aws/templates/create-dynamic-salt-files.sh index 682d9fbbc3b..33050b3591c 100644 --- a/cluster/aws/templates/create-dynamic-salt-files.sh +++ b/cluster/aws/templates/create-dynamic-salt-files.sh @@ -52,3 +52,14 @@ known_tokens_file="/srv/salt-overlay/salt/kube-apiserver/known_tokens.csv" mkdir -p /srv/salt-overlay/salt/kubelet kubelet_auth_file="/srv/salt-overlay/salt/kubelet/kubernetes_auth" (umask u=rw,go= ; echo "{\"BearerToken\": \"$kubelet_token\", \"Insecure\": true }" > $kubelet_auth_file) + +# Generate tokens for other "service accounts". Append to known_tokens. +# +# NB: If this list ever changes, this script actually has to +# change to detect the existence of this file, kill any deleted +# old tokens and add any new tokens (to handle the upgrade case). +local -r service_accounts=("system:scheduler" "system:controller_manager" "system:logging" "system:monitoring" "system:dns") +for account in "${service_accounts[@]}"; do + token=$(dd if=/dev/urandom bs=128 count=1 2>/dev/null | base64 | tr -d "=+/" | dd bs=32 count=1 2>/dev/null) + echo "${token},${account},${account}" >> "${KNOWN_TOKENS_FILE}" +done diff --git a/cluster/azure/util.sh b/cluster/azure/util.sh index 515917bea95..1c2900767b9 100644 --- a/cluster/azure/util.sh +++ b/cluster/azure/util.sh @@ -438,21 +438,22 @@ function kube-up { create-kubeconfig ) - # Wait for salt on the minions - sleep 30 - echo "Sanity checking cluster..." + echo + echo " This will continually check the minions to ensure docker is" + echo " installed. This is usually a good indicator that salt has" + echo " successfully provisioned. This might loop forever if there was" + echo " some uncaught error during start up." + echo # Basic sanity checking for (( i=0; i<${#MINION_NAMES[@]}; i++)); do # Make sure docker is installed echo "--> Making sure docker is installed on ${MINION_NAMES[$i]}." - ssh -oStrictHostKeyChecking=no -i $AZ_SSH_KEY -p ${ssh_ports[$i]} \ - $AZ_CS.cloudapp.net which docker > /dev/null || { - echo "Docker failed to install on ${MINION_NAMES[$i]}. Your cluster is unlikely" >&2 - echo "to work correctly. Please run ./cluster/kube-down.sh and re-create the" >&2 - echo "cluster. (sorry!)" >&2 - exit 1 - } + until ssh -oStrictHostKeyChecking=no -i $AZ_SSH_KEY -p ${ssh_ports[$i]} \ + $AZ_CS.cloudapp.net which docker > /dev/null 2>&1; do + printf "." + sleep 2 + done done echo diff --git a/cluster/gce/util.sh b/cluster/gce/util.sh index 6a3811dbd0a..33c59c46921 100755 --- a/cluster/gce/util.sh +++ b/cluster/gce/util.sh @@ -25,6 +25,7 @@ source "${KUBE_ROOT}/cluster/common.sh" NODE_INSTANCE_PREFIX="${INSTANCE_PREFIX}-minion" KUBE_PROMPT_FOR_UPDATE=y +KUBE_SKIP_UPDATE=${KUBE_SKIP_UPDATE-"n"} # Verify prereqs function verify-prereqs { @@ -48,12 +49,20 @@ function verify-prereqs { fi fi done + if [[ "${KUBE_SKIP_UPDATE}" == "y" ]]; then + return + fi # update and install components as needed if [[ "${KUBE_PROMPT_FOR_UPDATE}" != "y" ]]; then gcloud_prompt="-q" fi - gcloud ${gcloud_prompt:-} components update preview || true - gcloud ${gcloud_prompt:-} components update || true + local sudo_prefix="" + if [ ! -w $(dirname `which gcloud`) ]; then + sudo_prefix="sudo" + fi + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update preview || true + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update alpha || true + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update || true } # Create a temp dir that'll be deleted at the end of this bash session. @@ -967,11 +976,15 @@ function test-teardown { function ssh-to-node { local node="$1" local cmd="$2" + # Loop until we can successfully ssh into the box for try in $(seq 1 5); do - if gcloud compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" --zone="${ZONE}" "${node}" --command "${cmd}"; then + if gcloud compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" --zone="${ZONE}" "${node}" --command "echo test"; then break fi + sleep 5 done + # Then actually try the command. + gcloud compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" --zone="${ZONE}" "${node}" --command "${cmd}" } # Restart the kube-proxy on a node ($1) @@ -981,7 +994,7 @@ function restart-kube-proxy { # Restart the kube-apiserver on a node ($1) function restart-apiserver { - ssh-to-node "$1" "sudo docker kill `sudo docker ps | grep /kube-apiserver | awk '{print $1}'`" + ssh-to-node "$1" "sudo docker kill \`sudo docker ps | grep /kube-apiserver | awk '{print $1}'\`" } # Perform preparations required to run e2e tests diff --git a/cluster/gke/util.sh b/cluster/gke/util.sh index f8422386ca8..754e5fe27a2 100755 --- a/cluster/gke/util.sh +++ b/cluster/gke/util.sh @@ -20,6 +20,7 @@ # config-default.sh. KUBE_PROMPT_FOR_UPDATE=y +KUBE_SKIP_UPDATE=${KUBE_SKIP_UPDATE-"n"} KUBE_ROOT=$(dirname "${BASH_SOURCE}")/../.. source "${KUBE_ROOT}/cluster/gke/${KUBE_CONFIG_FILE:-config-default.sh}" @@ -86,13 +87,20 @@ function verify-prereqs() { exit 1 fi fi + if [[ "${KUBE_SKIP_UPDATE}" == "y" ]]; then + return + fi # update and install components as needed if [[ "${KUBE_PROMPT_FOR_UPDATE}" != "y" ]]; then gcloud_prompt="-q" fi - gcloud ${gcloud_prompt:-} components update preview || true - gcloud ${gcloud_prompt:-} components update alpha || true - gcloud ${gcloud_prompt:-} components update || true + local sudo_prefix="" + if [ ! -w $(dirname `which gcloud`) ]; then + sudo_prefix="sudo" + fi + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update preview || true + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update alpha|| true + ${sudo_prefix} gcloud ${gcloud_prompt:-} components update || true } # Instantiate a kubernetes cluster @@ -238,8 +246,15 @@ function ssh-to-node() { local node="$1" local cmd="$2" - "${GCLOUD}" compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" \ - --zone="${ZONE}" "${node}" --command "${cmd}" + # Loop until we can successfully ssh into the box + for try in $(seq 1 5); do + if gcloud compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" --zone="${ZONE}" "${node}" --command "echo test"; then + break + fi + sleep 5 + done + # Then actually try the command. + gcloud compute ssh --ssh-flag="-o LogLevel=quiet" --project "${PROJECT}" --zone="${ZONE}" "${node}" --command "${cmd}" } # Restart the kube-proxy on a node ($1) @@ -251,7 +266,7 @@ function restart-kube-proxy() { # Restart the kube-proxy on master ($1) function restart-apiserver() { echo "... in restart-kube-apiserver()" >&2 - ssh-to-node "$1" "sudo docker kill `sudo docker ps | grep /kube-apiserver | awk '{print $1}'`" + ssh-to-node "$1" "sudo docker kill \`sudo docker ps | grep /kube-apiserver | awk '{print $1}'\`" } # Execute after running tests to perform any required clean-up. This is called diff --git a/cluster/vagrant/provision-master.sh b/cluster/vagrant/provision-master.sh index 774201ed8fd..0175cf70ed2 100755 --- a/cluster/vagrant/provision-master.sh +++ b/cluster/vagrant/provision-master.sh @@ -145,6 +145,17 @@ if [[ ! -f "${known_tokens_file}" ]]; then mkdir -p /srv/salt-overlay/salt/kubelet kubelet_auth_file="/srv/salt-overlay/salt/kubelet/kubernetes_auth" (umask u=rw,go= ; echo "{\"BearerToken\": \"$kubelet_token\", \"Insecure\": true }" > $kubelet_auth_file) + + # Generate tokens for other "service accounts". Append to known_tokens. + # + # NB: If this list ever changes, this script actually has to + # change to detect the existence of this file, kill any deleted + # old tokens and add any new tokens (to handle the upgrade case). + local -r service_accounts=("system:scheduler" "system:controller_manager" "system:logging" "system:monitoring" "system:dns") + for account in "${service_accounts[@]}"; do + token=$(dd if=/dev/urandom bs=128 count=1 2>/dev/null | base64 | tr -d "=+/" | dd bs=32 count=1 2>/dev/null) + echo "${token},${account},${account}" >> "${KNOWN_TOKENS_FILE}" + done fi # Configure nginx authorization diff --git a/cluster/validate-cluster.sh b/cluster/validate-cluster.sh index c4b37da848c..ac0e638af0a 100755 --- a/cluster/validate-cluster.sh +++ b/cluster/validate-cluster.sh @@ -59,10 +59,9 @@ while true; do # Parse the output to capture the value of the second column("HEALTH"), then use grep to # count the number of times it doesn't match "success". # Because of the header, the actual unsuccessful count is 1 minus the count. - non_success_count=$(echo "${kubectl_output}" | \ - sed -n 's/^\([[:alnum:][:punct:]]\+\)\s\+\([[:alnum:][:punct:]]\+\)\s\+.*/\2/p' | \ - grep 'Healthy' --invert-match -c) + sed -n 's/^[[:alnum:][:punct:]]/&/p' | \ + grep --invert-match -c '^[[:alnum:][:punct:]]\{1,\}[[:space:]]\{1,\}Healthy') if ((non_success_count > 1)); then if ((attempt < 5)); then diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index 676deee188f..27c5b6c0c68 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -43,6 +43,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/nodecontroller" replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" @@ -260,7 +261,7 @@ func podsOnMinions(c *client.Client, podNamespace string, labelSelector labels.S podInfo := fakeKubeletClient{} // wait for minions to indicate they have info about the desired pods return func() (bool, error) { - pods, err := c.Pods(podNamespace).List(labelSelector) + pods, err := c.Pods(podNamespace).List(labelSelector, fields.Everything()) if err != nil { glog.Infof("Unable to get pods to list: %v", err) return false, nil @@ -384,7 +385,7 @@ containers: namespace := kubelet.NamespaceDefault if err := wait.Poll(time.Second, time.Minute*2, podRunning(c, namespace, podName)); err != nil { - if pods, err := c.Pods(namespace).List(labels.Everything()); err == nil { + if pods, err := c.Pods(namespace).List(labels.Everything(), fields.Everything()); err == nil { for _, pod := range pods.Items { glog.Infof("pod found: %s/%s", namespace, pod.Name) } diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index 92e2ba8b281..d6aad693fae 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -35,6 +35,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/nodecontroller" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/servicecontroller" replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" + "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" @@ -42,6 +43,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" + "github.com/prometheus/client_golang/prometheus" "github.com/spf13/pflag" ) @@ -179,13 +181,20 @@ func (s *CMServer) Run(_ []string) error { } go func() { + mux := http.NewServeMux() + healthz.InstallHandler(mux) if s.EnableProfiling { - mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) } - http.ListenAndServe(net.JoinHostPort(s.Address.String(), strconv.Itoa(s.Port)), nil) + mux.Handle("/metrics", prometheus.Handler()) + + server := &http.Server{ + Addr: net.JoinHostPort(s.Address.String(), strconv.Itoa(s.Port)), + Handler: mux, + } + glog.Fatal(server.ListenAndServe()) }() endpoints := service.NewEndpointController(kubeClient) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 5bf6c2c46e6..cc061afcd4a 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -251,6 +251,7 @@ _kubectl_get() must_have_one_noun+=("persistentvolume") must_have_one_noun+=("persistentvolumeclaim") must_have_one_noun+=("pod") + must_have_one_noun+=("podtemplate") must_have_one_noun+=("replicationcontroller") must_have_one_noun+=("resourcequota") must_have_one_noun+=("secret") diff --git a/docs/design/architecture.md b/docs/design/architecture.md index 06a0a0ef0ae..3f021aaf35e 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -17,7 +17,7 @@ The **Kubelet** manages [pods](../pods.md) and their containers, their images, t ### Kube-Proxy -Each node also runs a simple network proxy and load balancer (see the [services FAQ](https://github.com/GoogleCloudPlatform/kubernetes/wiki/Services-FAQ) for more details). This reflects `services` (see [the services doc](../docs/services.md) for more details) as defined in the Kubernetes API on each node and can do simple TCP and UDP stream forwarding (round robin) across a set of backends. +Each node also runs a simple network proxy and load balancer (see the [services FAQ](https://github.com/GoogleCloudPlatform/kubernetes/wiki/Services-FAQ) for more details). This reflects `services` (see [the services doc](../services.md) for more details) as defined in the Kubernetes API on each node and can do simple TCP and UDP stream forwarding (round robin) across a set of backends. Service endpoints are currently found via [DNS](../dns.md) or through environment variables (both [Docker-links-compatible](https://docs.docker.com/userguide/dockerlinks/) and Kubernetes {FOO}_SERVICE_HOST and {FOO}_SERVICE_PORT variables are supported). These variables resolve to ports managed by the service proxy. diff --git a/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-etcd-node-template.yml b/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-etcd-node-template.yml index f6e9fcda92a..bf138e36794 100644 --- a/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-etcd-node-template.yml +++ b/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-etcd-node-template.yml @@ -37,6 +37,7 @@ coreos: Environment=ETCD_INITIAL_ADVERTISE_PEER_URLS=http://%host%:2380 Environment=ETCD_LISTEN_PEER_URLS=http://%host%:2380 Environment=ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379,http://0.0.0.0:4001 + Environment=ETCD_ADVERTISE_CLIENT_URLS=http://%host%:2379,http://%host%:4001 Environment=ETCD_INITIAL_CLUSTER=%cluster% Environment=ETCD_INITIAL_CLUSTER_STATE=new ExecStart=/opt/bin/etcd2 diff --git a/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-main-nodes-template.yml b/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-main-nodes-template.yml index 7c9a4d3cba7..d5396783035 100644 --- a/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-main-nodes-template.yml +++ b/docs/getting-started-guides/coreos/azure/cloud_config_templates/kubernetes-cluster-main-nodes-template.yml @@ -129,7 +129,7 @@ coreos: ExecStartPre=/usr/bin/curl \ --silent \ --location \ - https://github.com/zettio/weave/releases/download/latest_release/weave \ + https://github.com/weaveworks/weave/releases/download/v0.9.0/weave \ --output /opt/bin/weave ExecStartPre=/usr/bin/curl \ --silent \ diff --git a/docs/getting-started-guides/fedora/fedora_manual_config.md b/docs/getting-started-guides/fedora/fedora_manual_config.md index 51a38a03ed5..2032f26b693 100644 --- a/docs/getting-started-guides/fedora/fedora_manual_config.md +++ b/docs/getting-started-guides/fedora/fedora_manual_config.md @@ -77,6 +77,11 @@ KUBE_SERVICE_ADDRESSES="--portal_net=10.254.0.0/16" KUBE_API_ARGS="" ``` +* Edit /etc/etcd/etcd.conf,let the etcd to listen all the ip instead of 127.0.0.1,if not ,you will get the error like "connection refused" +``` +ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:4001" +``` + * Start the appropriate services on master: ``` diff --git a/examples/examples_test.go b/examples/examples_test.go index d21c1eea150..dbf19b025a8 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -34,7 +34,6 @@ import ( ) func validateObject(obj runtime.Object) (errors []error) { - ctx := api.NewDefaultContext() switch t := obj.(type) { case *api.ReplicationController: if t.Namespace == "" { @@ -49,7 +48,6 @@ func validateObject(obj runtime.Object) (errors []error) { if t.Namespace == "" { t.Namespace = api.NamespaceDefault } - api.ValidNamespace(ctx, &t.ObjectMeta) errors = validation.ValidateService(t) case *api.ServiceList: for i := range t.Items { @@ -59,7 +57,6 @@ func validateObject(obj runtime.Object) (errors []error) { if t.Namespace == "" { t.Namespace = api.NamespaceDefault } - api.ValidNamespace(ctx, &t.ObjectMeta) errors = validation.ValidatePod(t) case *api.PodList: for i := range t.Items { @@ -68,8 +65,15 @@ func validateObject(obj runtime.Object) (errors []error) { case *api.PersistentVolume: errors = validation.ValidatePersistentVolume(t) case *api.PersistentVolumeClaim: - api.ValidNamespace(ctx, &t.ObjectMeta) + if t.Namespace == "" { + t.Namespace = api.NamespaceDefault + } errors = validation.ValidatePersistentVolumeClaim(t) + case *api.PodTemplate: + if t.Namespace == "" { + t.Namespace = api.NamespaceDefault + } + errors = validation.ValidatePodTemplate(t) default: return []error{fmt.Errorf("no validation defined for %#v", obj)} } @@ -156,6 +160,7 @@ func TestExampleObjectSchemas(t *testing.T) { "pod-with-http-healthcheck": &api.Pod{}, "service": &api.Service{}, "replication-controller": &api.ReplicationController{}, + "podtemplate": &api.PodTemplate{}, }, "../examples/update-demo": { "kitten-rc": &api.ReplicationController{}, diff --git a/examples/meteor/README.md b/examples/meteor/README.md new file mode 100644 index 00000000000..5fb3eab297a --- /dev/null +++ b/examples/meteor/README.md @@ -0,0 +1,72 @@ +Build a container for your Meteor app +------------------------------------- + +To be able to run your Meteor app on Kubernetes you need to build a container for it first. To do that you need to install [Docker](https://www.docker.com) and get an account on [Docker Hub](https://hub.docker.com/). Once you have that you need to add 2 files to your Meteor project "Dockerfile" and ".dockerignore". + +"Dockerfile" should contain this: + + FROM chees/meteor-kubernetes + ENV ROOT_URL http://myawesomeapp.com + +You should replace the ROOT_URL with the actual hostname of your app. + +The .dockerignore file should contain this: + + .meteor/local + packages/*/.build* + +This tells Docker to ignore the files on those directories when it's building your container. + +You can see an example of a Dockerfile in our [meteor-gke-example](https://github.com/Q42/meteor-gke-example) project. + +Now you can build your container by running something like this in your Meteor project directory: + + docker build -t chees/meteor-gke-example:1 . + +Here you should replace "chees" with your own username on Docker Hub, "meteor-gke-example" with the name of your project and "1" with the version name of your build. + +Push the container to your Docker hub account (replace the username and project with your own again): + + docker push chees/meteor-gke-example + + + +Running +------- + +Now that you have containerized your Meteor app it's time to set up your cluster. Edit "meteor-controller.json" and make sure the "image" points to the container you just pushed to the Docker Hub. + +For Mongo we use a Persistent Disk to store the data. If you're using gcloud you can create it once by running: + + gcloud compute disks create --size=200GB mongo-disk + +You also need to format the disk before you can use it: + + gcloud compute instances attach-disk --disk=mongo-disk --device-name temp-data k8s-meteor-master + gcloud compute ssh k8s-meteor-master --command "sudo mkdir /mnt/tmp && sudo /usr/share/google/safe_format_and_mount /dev/disk/by-id/google-temp-data /mnt/tmp" + gcloud compute instances detach-disk --disk mongo-disk k8s-meteor-master + +Now you can start Mongo using that disk: + + kubectl create -f mongo-pod.json + kubectl create -f mongo-service.json + +Wait until Mongo is started completely and then set up Meteor: + + kubectl create -f meteor-controller.json + kubectl create -f meteor-service.json + +Note that meteor-service.json creates an external load balancer, so your app should be available through the IP of that load balancer once the Meteor pods are started. On gcloud you can find the IP of your load balancer by running: + + gcloud compute forwarding-rules list k8s-meteor-default-meteor | grep k8s-meteor-default-meteor | awk '{print $3}' + +You might have to open up port 80 if it's not open yet in your project. For example: + + gcloud compute firewall-rules create meteor-80 --allow=tcp:80 --target-tags k8s-meteor-node + + + + +TODO replace the mongo image with the official mongo? https://registry.hub.docker.com/_/mongo/ +TODO use Kubernetes v1beta3 syntax? + diff --git a/examples/meteor/dockerbase/Dockerfile b/examples/meteor/dockerbase/Dockerfile new file mode 100644 index 00000000000..8ce633c634b --- /dev/null +++ b/examples/meteor/dockerbase/Dockerfile @@ -0,0 +1,18 @@ +FROM node:0.10 +MAINTAINER Christiaan Hees + +ONBUILD WORKDIR /appsrc +ONBUILD COPY . /appsrc + +ONBUILD RUN curl https://install.meteor.com/ | sh && \ + meteor build ../app --directory --architecture os.linux.x86_64 && \ + rm -rf /appsrc +# TODO rm meteor so it doesn't take space in the image? + +ONBUILD WORKDIR /app/bundle + +ONBUILD RUN (cd programs/server && npm install) +EXPOSE 8080 +CMD [] +ENV PORT 8080 +ENTRYPOINT MONGO_URL=mongodb://$MONGO_SERVICE_HOST:$MONGO_SERVICE_PORT /usr/local/bin/node main.js diff --git a/examples/meteor/dockerbase/README.md b/examples/meteor/dockerbase/README.md new file mode 100644 index 00000000000..f0234c9ad04 --- /dev/null +++ b/examples/meteor/dockerbase/README.md @@ -0,0 +1,9 @@ +Building the meteor-kubernetes base image +----------------------------------------- + +As a normal user you don't need to do this since the image is already built and pushed to Docker Hub. You can just use it as a base image. See [this example](https://github.com/Q42/meteor-gke-example/blob/master/Dockerfile). + +To build and push the base meteor-kubernetes image: + + docker build -t chees/meteor-kubernetes . + docker push chees/meteor-kubernetes diff --git a/examples/meteor/meteor-controller.json b/examples/meteor/meteor-controller.json new file mode 100644 index 00000000000..d2a08ad2d7c --- /dev/null +++ b/examples/meteor/meteor-controller.json @@ -0,0 +1,26 @@ +{ + "id": "meteor-controller", + "kind": "ReplicationController", + "apiVersion": "v1beta1", + "desiredState": { + "replicas": 2, + "replicaSelector": {"name": "meteor"}, + "podTemplate": { + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "meteor-controller", + "containers": [{ + "name": "meteor", + "image": "chees/meteor-gke-example:latest", + "cpu": 1000, + "memory": 500000000, + "ports": [{"name": "http-server", "containerPort": 8080, "hostPort": 80}] + }] + } + }, + "labels": { "name": "meteor" } + } + }, + "labels": {"name": "meteor"} +} diff --git a/examples/meteor/meteor-service.json b/examples/meteor/meteor-service.json new file mode 100644 index 00000000000..11c0ca41da7 --- /dev/null +++ b/examples/meteor/meteor-service.json @@ -0,0 +1,10 @@ +{ + "apiVersion": "v1beta1", + "kind": "Service", + "id": "meteor", + "port": 80, + "containerPort": "http-server", + "selector": { "name": "meteor" }, + "createExternalLoadBalancer": true, + "sessionAffinity": "ClientIP" +} diff --git a/examples/meteor/mongo-pod.json b/examples/meteor/mongo-pod.json new file mode 100644 index 00000000000..c9a2d5051b0 --- /dev/null +++ b/examples/meteor/mongo-pod.json @@ -0,0 +1,33 @@ +{ + "id": "mongo", + "kind": "Pod", + "apiVersion": "v1beta1", + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "mongo", + "containers": [{ + "name": "mongo", + "image": "mongo", + "cpu": 1000, + "ports": [{ "name": "mongo", "containerPort": 27017 }], + "volumeMounts": [{ + "mountPath": "/data/db", + "name": "mongo-disk" + }] + }], + "volumes": [{ + "name": "mongo-disk", + "source": { + "persistentDisk": { + "pdName": "mongo-disk", + "fsType": "ext4" + } + } + }] + } + }, + "labels": { + "name": "mongo", "role": "mongo" + } +} diff --git a/examples/meteor/mongo-service.json b/examples/meteor/mongo-service.json new file mode 100644 index 00000000000..dde2e5066d2 --- /dev/null +++ b/examples/meteor/mongo-service.json @@ -0,0 +1,13 @@ +{ + "id": "mongo", + "kind": "Service", + "apiVersion": "v1beta1", + "port": 27017, + "containerPort": "mongo", + "selector": { + "name": "mongo", "role": "mongo" + }, + "labels": { + "name": "mongo" + } +} \ No newline at end of file diff --git a/examples/walkthrough/podtemplate.json b/examples/walkthrough/podtemplate.json new file mode 100644 index 00000000000..5732a113584 --- /dev/null +++ b/examples/walkthrough/podtemplate.json @@ -0,0 +1,22 @@ + { + "apiVersion": "v1beta3", + "kind": "PodTemplate", + "metadata": { + "name": "nginx" + }, + "template": { + "metadata": { + "labels": { + "name": "nginx" + }, + "generateName": "nginx-" + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "dockerfile/nginx", + "ports": [{"containerPort": 80}] + }] + } + } + } diff --git a/hack/jenkins/e2e.sh b/hack/jenkins/e2e.sh index 2e07193a4e6..785f6c9e69f 100755 --- a/hack/jenkins/e2e.sh +++ b/hack/jenkins/e2e.sh @@ -102,10 +102,27 @@ else exit 1 fi + # Tell kube-up.sh to skip the update, it doesn't lock. An internal + # gcloud bug can cause racing component updates to stomp on each + # other. + export KUBE_SKIP_UPDATE=y sudo flock -x -n /var/run/lock/gcloud-components.lock -c "gcloud components update -q" || true - GITHASH=$(gsutil cat gs://kubernetes-release/ci/latest.txt) - gsutil -m cp gs://kubernetes-release/ci/${GITHASH}/kubernetes.tar.gz gs://kubernetes-release/ci/${GITHASH}/kubernetes-test.tar.gz . + # The "ci" bucket is for builds like "v0.15.0-468-gfa648c1" + bucket="ci" + # The "latest" version picks the most recent "ci" or "release" build. + version_file="latest" + if [[ ${JENKINS_USE_RELEASE_TARS:-} =~ ^[yY]$ ]]; then + # The "release" bucket is for builds like "v0.15.0" + bucket="release" + if [[ ${JENKINS_USE_STABLE:-} =~ ^[yY]$ ]]; then + # The "stable" version picks the most recent "release" build. + version_file="stable" + fi + fi + + githash=$(gsutil cat gs://kubernetes-release/${bucket}/${version_file}.txt) + gsutil -m cp gs://kubernetes-release/${bucket}/${githash}/kubernetes.tar.gz gs://kubernetes-release/${bucket}/${githash}/kubernetes-test.tar.gz . fi md5sum kubernetes*.tar.gz @@ -115,9 +132,9 @@ cd kubernetes # Set by GKE-CI to change the CLUSTER_API_VERSION to the git version if [[ ! -z ${E2E_SET_CLUSTER_API_VERSION:-} ]]; then - export CLUSTER_API_VERSION=$(echo ${GITHASH} | cut -c 2-) -elif [[ ! -z ${E2E_USE_LATEST_RELEASE_VERSION:-} ]]; then - release=$(gsutil cat gs://kubernetes-release/release/latest.txt | cut -c 2-) + export CLUSTER_API_VERSION=$(echo ${githash} | cut -c 2-) +elif [[ ${JENKINS_USE_RELEASE_TARS:-} =~ ^[yY]$ ]]; then + release=$(gsutil cat gs://kubernetes-release/release/${version_file}.txt | cut -c 2-) export CLUSTER_API_VERSION=${release} fi diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index b86756a9a02..3e534329de2 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -358,6 +358,34 @@ for version in "${kube_api_versions[@]}"; do kube::test::get_object_assert 'pods --namespace=other' "{{range.items}}{{$id_field}}:{{end}}" '' + ################# + # Pod templates # + ################# + + # Note: pod templates exist only in v1beta3 and above, so output will always be in that form + + ### Create PODTEMPLATE + # Pre-condition: no PODTEMPLATE + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" '' + # Command + kubectl create -f examples/walkthrough/podtemplate.json "${kube_flags[@]}" + # Post-condition: nginx PODTEMPLATE is available + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" 'nginx:' + + ### Printing pod templates works + kubectl get podtemplates "${kube_flags[@]}" + ### Display of an object which doesn't existing in v1beta1 and v1beta2 works + [[ "$(kubectl get podtemplates -o yaml "${kube_flags[@]}" | grep nginx)" ]] + + ### Delete nginx pod template by name + # Pre-condition: nginx pod template is available + kube::test::get_object_assert podtemplates "{{range.items}}{{.metadata.name}}:{{end}}" 'nginx:' + # Command + kubectl delete podtemplate nginx "${kube_flags[@]}" + # Post-condition: No templates exist + kube::test::get_object_assert podtemplate "{{range.items}}{{.metadata.name}}:{{end}}" '' + + ############ # Services # ############ diff --git a/pkg/api/latest/latest_test.go b/pkg/api/latest/latest_test.go index 724f5ee59d9..6b1591b5117 100644 --- a/pkg/api/latest/latest_test.go +++ b/pkg/api/latest/latest_test.go @@ -80,6 +80,10 @@ func TestRESTMapper(t *testing.T) { t.Errorf("unexpected version mapping: %s %s %v", v, k, err) } + if m, err := RESTMapper.RESTMapping("PodTemplate", ""); err != nil || m.APIVersion != "v1beta3" || m.Resource != "podtemplates" { + t.Errorf("unexpected version mapping: %#v %v", m, err) + } + for _, version := range Versions { mapping, err := RESTMapper.RESTMapping("ReplicationController", version) if err != nil { diff --git a/pkg/api/register.go b/pkg/api/register.go index 83fc40e23af..823f8bb7685 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -28,6 +28,8 @@ func init() { &Pod{}, &PodList{}, &PodStatusResult{}, + &PodTemplate{}, + &PodTemplateList{}, &ReplicationControllerList{}, &ReplicationController{}, &ServiceList{}, @@ -71,6 +73,8 @@ func init() { func (*Pod) IsAnAPIObject() {} func (*PodList) IsAnAPIObject() {} func (*PodStatusResult) IsAnAPIObject() {} +func (*PodTemplate) IsAnAPIObject() {} +func (*PodTemplateList) IsAnAPIObject() {} func (*ReplicationController) IsAnAPIObject() {} func (*ReplicationControllerList) IsAnAPIObject() {} func (*Service) IsAnAPIObject() {} diff --git a/pkg/api/rest/resttest/resttest.go b/pkg/api/rest/resttest/resttest.go index 29585d7dc7e..2732a0e563f 100644 --- a/pkg/api/rest/resttest/resttest.go +++ b/pkg/api/rest/resttest/resttest.go @@ -179,7 +179,7 @@ func (t *Tester) TestCreateRejectsMismatchedNamespace(valid runtime.Object) { if err == nil { t.Errorf("Expected an error, but we didn't get one") } else if strings.Contains(err.Error(), "Controller.Namespace does not match the provided context") { - t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) + t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err) } } @@ -195,7 +195,30 @@ func (t *Tester) TestCreateRejectsNamespace(valid runtime.Object) { if err == nil { t.Errorf("Expected an error, but we didn't get one") } else if strings.Contains(err.Error(), "Controller.Namespace does not match the provided context") { - t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err.Error()) + t.Errorf("Expected 'Controller.Namespace does not match the provided context' error, got '%v'", err) + } +} + +func (t *Tester) TestUpdate(valid runtime.Object, existing, older runtime.Object) { + t.TestUpdateFailsOnNotFound(copyOrDie(valid)) + t.TestUpdateFailsOnVersion(copyOrDie(older)) +} + +func (t *Tester) TestUpdateFailsOnNotFound(valid runtime.Object) { + _, _, err := t.storage.(rest.Updater).Update(api.NewDefaultContext(), valid) + if err == nil { + t.Errorf("Expected an error, but we didn't get one") + } else if !errors.IsNotFound(err) { + t.Errorf("Expected NotFound error, got '%v'", err) + } +} + +func (t *Tester) TestUpdateFailsOnVersion(older runtime.Object) { + _, _, err := t.storage.(rest.Updater).Update(api.NewDefaultContext(), older) + if err == nil { + t.Errorf("Expected an error, but we didn't get one") + } else if !errors.IsConflict(err) { + t.Errorf("Expected Conflict error, got '%v'", err) } } diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index ca79e6f9408..e9f65233307 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -85,13 +85,20 @@ func roundTrip(t *testing.T, codec runtime.Codec, item runtime.Object) { } // roundTripSame verifies the same source object is tested in all API versions. -func roundTripSame(t *testing.T, item runtime.Object) { +func roundTripSame(t *testing.T, item runtime.Object, except ...string) { + set := util.NewStringSet(except...) seed := rand.Int63() fuzzInternalObject(t, "", item, seed) - roundTrip(t, v1beta1.Codec, item) - roundTrip(t, v1beta2.Codec, item) - fuzzInternalObject(t, "v1beta3", item, seed) - roundTrip(t, v1beta3.Codec, item) + if !set.Has("v1beta1") { + roundTrip(t, v1beta1.Codec, item) + } + if !set.Has("v1beta2") { + roundTrip(t, v1beta2.Codec, item) + } + if !set.Has("v1beta3") { + fuzzInternalObject(t, "v1beta3", item, seed) + roundTrip(t, v1beta3.Codec, item) + } } func roundTripAll(t *testing.T, item runtime.Object) { @@ -130,6 +137,10 @@ func TestList(t *testing.T) { var nonRoundTrippableTypes = util.NewStringSet("ContainerManifest", "ContainerManifestList") var nonInternalRoundTrippableTypes = util.NewStringSet("List", "ListOptions", "PodExecOptions") +var nonRoundTrippableTypesByVersion = map[string][]string{ + "PodTemplate": {"v1beta1", "v1beta2"}, + "PodTemplateList": {"v1beta1", "v1beta2"}, +} func TestRoundTripTypes(t *testing.T) { // api.Scheme.Log(t) @@ -148,7 +159,7 @@ func TestRoundTripTypes(t *testing.T) { if _, err := meta.TypeAccessor(item); err != nil { t.Fatalf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableTypes: %v", kind, err) } - roundTripSame(t, item) + roundTripSame(t, item, nonRoundTrippableTypesByVersion[kind]...) if !nonInternalRoundTrippableTypes.Has(kind) { roundTrip(t, api.Codec, fuzzInternalObject(t, "", item, rand.Int63())) } diff --git a/pkg/api/types.go b/pkg/api/types.go index 6dc455f7bb3..1aeb63f9f43 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -848,8 +848,8 @@ type PodTemplate struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty"` - // Spec defines the pods that will be created from this template - Spec PodTemplateSpec `json:"spec,omitempty"` + // Template defines the pods that will be created from this pod template + Template PodTemplateSpec `json:"template,omitempty"` } // PodTemplateList is a list of PodTemplates. diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index f9ecda57586..065fe3bb37d 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -854,8 +854,8 @@ type PodTemplate struct { TypeMeta `json:",inline"` ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - // Spec defines the behavior of a pod. - Spec PodTemplateSpec `json:"spec,omitempty" description:"specification of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` + // Template defines the pods that will be created from this pod template + Template PodTemplateSpec `json:"template,omitempty" description:"the template of the desired behavior of the pod; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } // PodTemplateList is a list of PodTemplates. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 78c0a9f4737..d2535dc1676 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -740,7 +740,7 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs cErrs = append(cErrs, validateEnv(ctr.Env).Prefix("env")...) cErrs = append(cErrs, validateVolumeMounts(ctr.VolumeMounts, volumes).Prefix("volumeMounts")...) cErrs = append(cErrs, validatePullPolicy(&ctr).Prefix("pullPolicy")...) - cErrs = append(cErrs, validateResourceRequirements(&ctr).Prefix("resources")...) + cErrs = append(cErrs, ValidateResourceRequirements(&ctr.Resources).Prefix("resources")...) allErrs = append(allErrs, cErrs.PrefixIndex(i)...) } // Check for colliding ports across all containers. @@ -888,6 +888,24 @@ func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) errs.ValidationErrorList { return allErrs } +// ValidatePodTemplate tests if required fields in the pod template are set. +func ValidatePodTemplate(pod *api.PodTemplate) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template, 0).Prefix("template")...) + return allErrs +} + +// ValidatePodTemplateUpdate tests to see if the update is legal for an end user to make. newPod is updated with fields +// that cannot be changed. +func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template, 0).Prefix("template")...) + return allErrs +} + var supportedSessionAffinityType = util.NewStringSet(string(api.AffinityTypeClientIP), string(api.AffinityTypeNone)) // ValidateService tests if required fields in the service are set. @@ -1167,9 +1185,9 @@ func validateBasicResource(quantity resource.Quantity) errs.ValidationErrorList } // Validates resource requirement spec. -func validateResourceRequirements(container *api.Container) errs.ValidationErrorList { +func ValidateResourceRequirements(requirements *api.ResourceRequirements) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} - for resourceName, quantity := range container.Resources.Limits { + for resourceName, quantity := range requirements.Limits { // Validate resource name. errs := validateResourceName(resourceName.String(), fmt.Sprintf("resources.limits[%s]", resourceName)) if api.IsStandardResourceName(resourceName.String()) { @@ -1177,7 +1195,14 @@ func validateResourceRequirements(container *api.Container) errs.ValidationError } allErrs = append(allErrs, errs...) } - + for resourceName, quantity := range requirements.Requests { + // Validate resource name. + errs := validateResourceName(resourceName.String(), fmt.Sprintf("resources.requests[%s]", resourceName)) + if api.IsStandardResourceName(resourceName.String()) { + errs = append(errs, validateBasicResource(quantity).Prefix(fmt.Sprintf("Resource %s: ", resourceName))...) + } + allErrs = append(allErrs, errs...) + } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index ddc27439946..ca10ea7b83a 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -954,6 +954,16 @@ func TestValidateContainers(t *testing.T) { ImagePullPolicy: "IfNotPresent", }, }, + "Resource Requests CPU invalid": { + { + Name: "abc-123", + Image: "image", + Resources: api.ResourceRequirements{ + Requests: getResourceLimits("-10", "0"), + }, + ImagePullPolicy: "IfNotPresent", + }, + }, "Resource Memory invalid": { { Name: "abc-123", @@ -1666,7 +1676,7 @@ func TestValidateService(t *testing.T) { func TestValidateReplicationControllerUpdate(t *testing.T) { validSelector := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: validSelector, }, @@ -1678,7 +1688,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { }, } readWriteVolumePodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: validSelector, }, @@ -1692,7 +1702,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { } invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, @@ -1712,7 +1722,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1720,7 +1730,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 3, Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, }, @@ -1729,7 +1739,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1737,7 +1747,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 1, Selector: validSelector, - Template: &readWriteVolumePodTemplate.Spec, + Template: &readWriteVolumePodTemplate.Template, }, }, }, @@ -1755,7 +1765,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1763,7 +1773,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: validSelector, - Template: &readWriteVolumePodTemplate.Spec, + Template: &readWriteVolumePodTemplate.Template, }, }, }, @@ -1772,7 +1782,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1780,7 +1790,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: invalidSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, }, @@ -1789,7 +1799,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1797,7 +1807,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: validSelector, - Template: &invalidPodTemplate.Spec, + Template: &invalidPodTemplate.Template, }, }, }, @@ -1806,7 +1816,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, update: api.ReplicationController{ @@ -1814,7 +1824,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: -1, Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, }, @@ -1830,7 +1840,7 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { func TestValidateReplicationController(t *testing.T) { validSelector := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: validSelector, }, @@ -1842,7 +1852,7 @@ func TestValidateReplicationController(t *testing.T) { }, } readWriteVolumePodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: validSelector, }, @@ -1856,7 +1866,7 @@ func TestValidateReplicationController(t *testing.T) { } invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, @@ -1871,14 +1881,14 @@ func TestValidateReplicationController(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, { ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, { @@ -1886,7 +1896,7 @@ func TestValidateReplicationController(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 1, Selector: validSelector, - Template: &readWriteVolumePodTemplate.Spec, + Template: &readWriteVolumePodTemplate.Template, }, }, } @@ -1901,27 +1911,27 @@ func TestValidateReplicationController(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, "missing-namespace": { ObjectMeta: api.ObjectMeta{Name: "abc-123"}, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, "empty selector": { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, "selector_doesnt_match": { ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, Spec: api.ReplicationControllerSpec{ Selector: map[string]string{"foo": "bar"}, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, "invalid manifest": { @@ -1935,7 +1945,7 @@ func TestValidateReplicationController(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: validSelector, - Template: &readWriteVolumePodTemplate.Spec, + Template: &readWriteVolumePodTemplate.Template, }, }, "negative_replicas": { @@ -1955,7 +1965,7 @@ func TestValidateReplicationController(t *testing.T) { }, Spec: api.ReplicationControllerSpec{ Selector: validSelector, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, "invalid_label 2": { @@ -1967,7 +1977,20 @@ func TestValidateReplicationController(t *testing.T) { }, }, Spec: api.ReplicationControllerSpec{ - Template: &invalidPodTemplate.Spec, + Template: &invalidPodTemplate.Template, + }, + }, + "invalid_annotation": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + Annotations: map[string]string{ + "NoUppercaseOrSpecialCharsLike=Equals": "bar", + }, + }, + Spec: api.ReplicationControllerSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, }, }, "invalid restart policy 1": { diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index ab0d83e6d8d..cd20d35af8f 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -104,8 +104,7 @@ func RateLimit(rl util.RateLimiter, handler http.Handler) http.Handler { func tooManyRequests(w http.ResponseWriter) { // Return a 429 status indicating "Too Many Requests" w.Header().Set("Retry-After", RetryAfter) - w.WriteHeader(errors.StatusTooManyRequests) - fmt.Fprintf(w, "Too many requests, please try again later.") + http.Error(w, "Too many requests, please try again later.", errors.StatusTooManyRequests) } // RecoverPanics wraps an http Handler to recover and log panics. @@ -113,8 +112,7 @@ func RecoverPanics(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { if x := recover(); x != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, "apis panic. Look in log for details.") + http.Error(w, "apis panic. Look in log for details.", http.StatusInternalServerError) glog.Infof("APIServer panic'd on %v %v: %v\n%s\n", req.Method, req.RequestURI, x, debug.Stack()) } }() diff --git a/pkg/client/pods.go b/pkg/client/pods.go index ced3c2679a0..b61c4ed4f0e 100644 --- a/pkg/client/pods.go +++ b/pkg/client/pods.go @@ -30,7 +30,7 @@ type PodsNamespacer interface { // PodInterface has methods to work with Pod resources. type PodInterface interface { - List(selector labels.Selector) (*api.PodList, error) + List(label labels.Selector, field fields.Selector) (*api.PodList, error) Get(name string) (*api.Pod, error) Delete(name string) error Create(pod *api.Pod) (*api.Pod, error) @@ -54,10 +54,10 @@ func newPods(c *Client, namespace string) *pods { } } -// List takes a selector, and returns the list of pods that match that selector. -func (c *pods) List(selector labels.Selector) (result *api.PodList, err error) { +// List takes label and field selectors, and returns the list of pods that match those selectors. +func (c *pods) List(label labels.Selector, field fields.Selector) (result *api.PodList, err error) { result = &api.PodList{} - err = c.r.Get().Namespace(c.ns).Resource("pods").LabelsSelectorParam(selector).Do().Into(result) + err = c.r.Get().Namespace(c.ns).Resource("pods").LabelsSelectorParam(label).FieldsSelectorParam(field).Do().Into(result) return } diff --git a/pkg/client/pods_test.go b/pkg/client/pods_test.go index 7316e2a573a..37a68e6c88b 100644 --- a/pkg/client/pods_test.go +++ b/pkg/client/pods_test.go @@ -22,6 +22,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" ) @@ -31,7 +32,7 @@ func TestListEmptyPods(t *testing.T) { Request: testRequest{Method: "GET", Path: testapi.ResourcePath("pods", ns, ""), Query: buildQueryValues(ns, nil)}, Response: Response{StatusCode: 200, Body: &api.PodList{}}, } - podList, err := c.Setup().Pods(ns).List(labels.Everything()) + podList, err := c.Setup().Pods(ns).List(labels.Everything(), fields.Everything()) c.Validate(t, podList, err) } @@ -57,7 +58,7 @@ func TestListPods(t *testing.T) { }, }, } - receivedPodList, err := c.Setup().Pods(ns).List(labels.Everything()) + receivedPodList, err := c.Setup().Pods(ns).List(labels.Everything(), fields.Everything()) c.Validate(t, receivedPodList, err) } @@ -91,7 +92,7 @@ func TestListPodsLabels(t *testing.T) { c.Setup() c.QueryValidator[labelSelectorQueryParamName] = validateLabels selector := labels.Set{"foo": "bar", "name": "baz"}.AsSelector() - receivedPodList, err := c.Pods(ns).List(selector) + receivedPodList, err := c.Pods(ns).List(selector, fields.Everything()) c.Validate(t, receivedPodList, err) } diff --git a/pkg/client/portforward/portforward.go b/pkg/client/portforward/portforward.go index 6cf92c54173..73d39dfcfaa 100644 --- a/pkg/client/portforward/portforward.go +++ b/pkg/client/portforward/portforward.go @@ -183,21 +183,13 @@ func (pf *PortForwarder) forward() error { return nil } -// listenOnPort creates a new listener on port and waits for new connections +// listenOnPort delegates listener creation and waits for new connections // in the background. func (pf *PortForwarder) listenOnPort(port *ForwardedPort) error { - listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port.Local)) + listener, err := pf.getListener("tcp", "localhost", port) if err != nil { return err } - parts := strings.Split(listener.Addr().String(), ":") - localPort, err := strconv.ParseUint(parts[1], 10, 16) - if err != nil { - return fmt.Errorf("Error parsing local part: %s", err) - } - port.Local = uint16(localPort) - glog.Infof("Forwarding from %d -> %d", localPort, port.Remote) - pf.listeners = append(pf.listeners, listener) go pf.waitForConnection(listener, *port) @@ -205,6 +197,27 @@ func (pf *PortForwarder) listenOnPort(port *ForwardedPort) error { return nil } +// getListener creates a listener on the interface targeted by the given hostname on the given port with +// the given protocol. protocol is in net.Listen style which basically admits values like tcp, tcp4, tcp6 +func (pf *PortForwarder) getListener(protocol string, hostname string, port *ForwardedPort) (net.Listener, error) { + listener, err := net.Listen(protocol, fmt.Sprintf("%s:%d", hostname, port.Local)) + if err != nil { + glog.Errorf("Unable to create listener: Error %s", err) + return nil, err + } + listenerAddress := listener.Addr().String() + host, localPort, _ := net.SplitHostPort(listenerAddress) + localPortUInt, err := strconv.ParseUint(localPort, 10, 16) + + if err != nil { + return nil, fmt.Errorf("Error parsing local port: %s from %s (%s)", err, listenerAddress, host) + } + port.Local = uint16(localPortUInt) + glog.Infof("Forwarding from %d -> %d", localPortUInt, port.Remote) + + return listener, nil +} + // waitForConnection waits for new connections to listener and handles them in // the background. func (pf *PortForwarder) waitForConnection(listener net.Listener, port ForwardedPort) { diff --git a/pkg/client/portforward/portforward_test.go b/pkg/client/portforward/portforward_test.go index 99d4ee43078..00132db46d8 100644 --- a/pkg/client/portforward/portforward_test.go +++ b/pkg/client/portforward/portforward_test.go @@ -209,6 +209,82 @@ func (s *fakeUpgradeStream) Headers() http.Header { return http.Header{} } +func TestGetListener(t *testing.T) { + var pf PortForwarder + testCases := []struct { + Hostname string + Protocol string + ShouldRaiseError bool + ExpectedListenerAddress string + }{ + { + Hostname: "localhost", + Protocol: "tcp4", + ShouldRaiseError: false, + ExpectedListenerAddress: "127.0.0.1", + }, + { + Hostname: "127.0.0.1", + Protocol: "tcp4", + ShouldRaiseError: false, + ExpectedListenerAddress: "127.0.0.1", + }, + { + Hostname: "[::1]", + Protocol: "tcp6", + ShouldRaiseError: false, + ExpectedListenerAddress: "::1", + }, + { + Hostname: "localhost", + Protocol: "tcp6", + ShouldRaiseError: false, + ExpectedListenerAddress: "::1", + }, + { + Hostname: "[::1]", + Protocol: "tcp4", + ShouldRaiseError: true, + }, + { + Hostname: "127.0.0.1", + Protocol: "tcp6", + ShouldRaiseError: true, + }, + } + + for i, testCase := range testCases { + expectedListenerPort := "12345" + listener, err := pf.getListener(testCase.Protocol, testCase.Hostname, &ForwardedPort{12345, 12345}) + errorRaised := err != nil + + if testCase.ShouldRaiseError != errorRaised { + t.Errorf("Test case #%d failed: Data %v an error has been raised(%t) where it should not (or reciprocally): %v", i, testCase, testCase.ShouldRaiseError, err) + continue + } + if errorRaised { + continue + } + + if listener == nil { + t.Errorf("Test case #%d did not raised an error (%t) but failed in initializing listener", i, err) + continue + } + + host, port, _ := net.SplitHostPort(listener.Addr().String()) + t.Logf("Asked a %s forward for: %s:%v, got listener %s:%s, expected: %s", testCase.Protocol, testCase.Hostname, 12345, host, port, expectedListenerPort) + if host != testCase.ExpectedListenerAddress { + t.Errorf("Test case #%d failed: Listener does not listen on exepected address: asked %v got %v", i, testCase.ExpectedListenerAddress, host) + } + if port != expectedListenerPort { + t.Errorf("Test case #%d failed: Listener does not listen on exepected port: asked %v got %v", i, expectedListenerPort, port) + + } + listener.Close() + + } +} + func TestForwardPorts(t *testing.T) { testCases := []struct { Upgrader *fakeUpgrader @@ -313,4 +389,5 @@ func TestForwardPorts(t *testing.T) { t.Fatalf("%d: expected conn closure", i) } } + } diff --git a/pkg/client/testclient/fake_pods.go b/pkg/client/testclient/fake_pods.go index 61bbfdf8123..9cba18ca77f 100644 --- a/pkg/client/testclient/fake_pods.go +++ b/pkg/client/testclient/fake_pods.go @@ -30,7 +30,7 @@ type FakePods struct { Namespace string } -func (c *FakePods) List(selector labels.Selector) (*api.PodList, error) { +func (c *FakePods) List(label labels.Selector, field fields.Selector) (*api.PodList, error) { obj, err := c.Fake.Invokes(FakeAction{Action: "list-pods"}, &api.PodList{}) return obj.(*api.PodList), err } diff --git a/pkg/cloudprovider/cloud.go b/pkg/cloudprovider/cloud.go index f3c3b18491d..57f9d110cf5 100644 --- a/pkg/cloudprovider/cloud.go +++ b/pkg/cloudprovider/cloud.go @@ -18,6 +18,7 @@ package cloudprovider import ( "net" + "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" ) @@ -44,8 +45,15 @@ type Clusters interface { // TODO(#6812): Use a shorter name that's less likely to be longer than cloud // providers' name length limits. -func GetLoadBalancerName(clusterName, serviceNamespace, serviceName string) string { - return clusterName + "-" + serviceNamespace + "-" + serviceName +func GetLoadBalancerName(service *api.Service) string { + //GCE requires that the name of a load balancer starts with a lower case letter. + ret := "a" + string(service.UID) + ret = strings.Replace(ret, "-", "", -1) + //AWS requires that the name of a load balancer is shorter than 32 bytes. + if len(ret) > 32 { + ret = ret[:32] + } + return ret } // TCPLoadBalancer is an abstract, pluggable interface for TCP load balancers. diff --git a/pkg/cloudprovider/nodecontroller/nodecontroller.go b/pkg/cloudprovider/nodecontroller/nodecontroller.go index 1fb0bb70e96..f7921abb80c 100644 --- a/pkg/cloudprovider/nodecontroller/nodecontroller.go +++ b/pkg/cloudprovider/nodecontroller/nodecontroller.go @@ -266,7 +266,7 @@ func (nc *NodeController) reconcileExternalServices(nodes *api.NodeList) (should glog.Errorf("External load balancers for non TCP services are not currently supported: %v.", service) continue } - name := cloudprovider.GetLoadBalancerName(nc.clusterName, service.Namespace, service.Name) + name := cloudprovider.GetLoadBalancerName(&service) err := balancer.UpdateTCPLoadBalancer(name, zone.Region, hosts) if err != nil { glog.Errorf("External error while updating TCP load balancer: %v.", err) @@ -652,7 +652,7 @@ func (nc *NodeController) getCloudNodesWithSpec() (*api.NodeList, error) { func (nc *NodeController) deletePods(nodeID string) error { glog.V(2).Infof("Delete all pods from %v", nodeID) // TODO: We don't yet have field selectors from client, see issue #1362. - pods, err := nc.kubeClient.Pods(api.NamespaceAll).List(labels.Everything()) + pods, err := nc.kubeClient.Pods(api.NamespaceAll).List(labels.Everything(), fields.Everything()) if err != nil { return err } diff --git a/pkg/cloudprovider/nodecontroller/nodecontroller_test.go b/pkg/cloudprovider/nodecontroller/nodecontroller_test.go index b174a3c8875..8155ed6c2b1 100644 --- a/pkg/cloudprovider/nodecontroller/nodecontroller_test.go +++ b/pkg/cloudprovider/nodecontroller/nodecontroller_test.go @@ -32,6 +32,7 @@ import ( fake_cloud "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/fake" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) @@ -609,7 +610,7 @@ func TestSyncCloudNodesReconcilesExternalService(t *testing.T) { // Set of nodes does not change: do nothing. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{newNode("node0"), newNode("node1")}, - Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", true), *newService("service1", false)}})}, + Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", types.UID(""), true), *newService("service1", types.UID(""), false)}})}, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0", "node1"}, }, @@ -621,28 +622,28 @@ func TestSyncCloudNodesReconcilesExternalService(t *testing.T) { // Delete "node1", target pool for "service0" should shrink. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{newNode("node0"), newNode("node1")}, - Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", true), *newService("service1", false)}})}, + Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", types.UID("2c104a7c-e79e-11e4-8187-42010af0068a"), true), *newService("service1", types.UID(""), false)}})}, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0"}, }, matchRE: ".*", expectedClientActions: []testclient.FakeAction{{Action: "list-pods"}, {Action: "list-services"}}, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ - {Name: "kubernetes-namespace-service0", Hosts: []string{"node0"}}, + {Name: "a2c104a7ce79e11e4818742010af0068", Hosts: []string{"node0"}}, }, }, { // Add "node1", target pool for "service0" should grow. fakeNodeHandler: &FakeNodeHandler{ Existing: []*api.Node{newNode("node0")}, - Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", true), *newService("service1", false)}})}, + Fake: testclient.NewSimpleFake(&api.ServiceList{Items: []api.Service{*newService("service0", types.UID("2c104a7c-e79e-11e4-8187-42010af0068a"), true), *newService("service1", types.UID(""), false)}})}, fakeCloud: &fake_cloud.FakeCloud{ Machines: []string{"node0", "node1"}, }, matchRE: ".*", expectedClientActions: []testclient.FakeAction{{Action: "list-services"}}, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ - {Name: "kubernetes-namespace-service0", Hosts: []string{"node0", "node1"}}, + {Name: "a2c104a7ce79e11e4818742010af0068", Hosts: []string{"node0", "node1"}}, }, }, } @@ -1128,8 +1129,8 @@ func newPod(name, host string) *api.Pod { return &api.Pod{ObjectMeta: api.ObjectMeta{Name: name}, Spec: api.PodSpec{Host: host}} } -func newService(name string, external bool) *api.Service { - return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace"}, Spec: api.ServiceSpec{CreateExternalLoadBalancer: external}} +func newService(name string, uid types.UID, external bool) *api.Service { + return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace", UID: uid}, Spec: api.ServiceSpec{CreateExternalLoadBalancer: external}} } func sortedNodeNames(nodes []*api.Node) []string { diff --git a/pkg/cloudprovider/servicecontroller/servicecontroller.go b/pkg/cloudprovider/servicecontroller/servicecontroller.go index 1d17323f10d..055eb84b1f1 100644 --- a/pkg/cloudprovider/servicecontroller/servicecontroller.go +++ b/pkg/cloudprovider/servicecontroller/servicecontroller.go @@ -419,7 +419,7 @@ func needsUpdate(oldService *api.Service, newService *api.Service) bool { } func (s *ServiceController) loadBalancerName(service *api.Service) string { - return cloudprovider.GetLoadBalancerName(s.clusterName, service.Namespace, service.Name) + return cloudprovider.GetLoadBalancerName(service) } func getTCPPorts(service *api.Service) ([]int, error) { diff --git a/pkg/controller/replication_controller.go b/pkg/controller/replication_controller.go index 0e43b909911..0b87356bb2a 100644 --- a/pkg/controller/replication_controller.go +++ b/pkg/controller/replication_controller.go @@ -223,7 +223,7 @@ func (s activePods) Less(i, j int) bool { func (rm *ReplicationManager) syncReplicationController(controller api.ReplicationController) error { s := labels.Set(controller.Spec.Selector).AsSelector() - podList, err := rm.kubeClient.Pods(controller.Namespace).List(s) + podList, err := rm.kubeClient.Pods(controller.Namespace).List(s, fields.Everything()) if err != nil { return err } diff --git a/pkg/conversion/conversion_test.go b/pkg/conversion/conversion_test.go new file mode 100644 index 00000000000..24897ecf89b --- /dev/null +++ b/pkg/conversion/conversion_test.go @@ -0,0 +1,97 @@ +/* +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 conversion_test + +import ( + "io/ioutil" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" +) + +func BenchmarkPodConversion(b *testing.B) { + data, err := ioutil.ReadFile("pod_example.json") + if err != nil { + b.Fatalf("unexpected error while reading file: %v", err) + } + var pod api.Pod + if err := api.Scheme.DecodeInto(data, &pod); err != nil { + b.Fatalf("unexpected error decoding pod: %v", err) + } + + scheme := api.Scheme.Raw() + for i := 0; i < b.N; i++ { + versionedObj, err := scheme.ConvertToVersion(&pod, testapi.Version()) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + _, err = scheme.ConvertToVersion(versionedObj, scheme.InternalVersion) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + } +} + +func BenchmarkNodeConversion(b *testing.B) { + data, err := ioutil.ReadFile("node_example.json") + if err != nil { + b.Fatalf("unexpected error while reading file: %v", err) + } + var node api.Node + if err := api.Scheme.DecodeInto(data, &node); err != nil { + b.Fatalf("unexpected error decoding node: %v", err) + } + + scheme := api.Scheme.Raw() + for i := 0; i < b.N; i++ { + versionedObj, err := scheme.ConvertToVersion(&node, testapi.Version()) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + _, err = scheme.ConvertToVersion(versionedObj, scheme.InternalVersion) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + } +} + +func BenchmarkReplicationControllerConversion(b *testing.B) { + data, err := ioutil.ReadFile("replication_controller_example.json") + if err != nil { + b.Fatalf("unexpected error while reading file: %v", err) + } + var replicationController api.ReplicationController + if err := api.Scheme.DecodeInto(data, &replicationController); err != nil { + b.Fatalf("unexpected error decoding node: %v", err) + } + + scheme := api.Scheme.Raw() + for i := 0; i < b.N; i++ { + versionedObj, err := scheme.ConvertToVersion(&replicationController, testapi.Version()) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + _, err = scheme.ConvertToVersion(versionedObj, scheme.InternalVersion) + if err != nil { + b.Fatalf("Conversion error: %v", err) + } + } +} diff --git a/pkg/conversion/node_example.json b/pkg/conversion/node_example.json new file mode 100644 index 00000000000..792995b84d7 --- /dev/null +++ b/pkg/conversion/node_example.json @@ -0,0 +1,49 @@ +{ + "kind": "Node", + "apiVersion": "v1beta3", + "metadata": { + "name": "e2e-test-wojtekt-minion-etd6", + "selfLink": "/api/v1beta1/nodes/e2e-test-wojtekt-minion-etd6", + "uid": "a7e89222-e8e5-11e4-8fde-42010af09327", + "resourceVersion": "379", + "creationTimestamp": "2015-04-22T11:49:39Z" + }, + "spec": { + "externalID": "15488322946290398375" + }, + "status": { + "capacity": { + "cpu": "1", + "memory": "1745152Ki" + }, + "conditions": [ + { + "type": "Ready", + "status": "True", + "lastHeartbeatTime": "2015-04-22T11:58:17Z", + "lastTransitionTime": "2015-04-22T11:49:52Z", + "reason": "kubelet is posting ready status" + } + ], + "addresses": [ + { + "type": "ExternalIP", + "address": "104.197.49.213" + }, + { + "type": "LegacyHostIP", + "address": "104.197.20.11" + } + ], + "nodeInfo": { + "machineID": "", + "systemUUID": "D59FA3FA-7B5B-7287-5E1A-1D79F13CB577", + "bootID": "44a832f3-8cfb-4de5-b7d2-d66030b6cd95", + "kernelVersion": "3.16.0-0.bpo.4-amd64", + "osImage": "Debian GNU/Linux 7 (wheezy)", + "containerRuntimeVersion": "docker://1.5.0", + "kubeletVersion": "v0.15.0-484-g0c8ee980d705a3-dirty", + "KubeProxyVersion": "v0.15.0-484-g0c8ee980d705a3-dirty" + } + } +} diff --git a/pkg/conversion/pod_example.json b/pkg/conversion/pod_example.json new file mode 100644 index 00000000000..0bfa6adab9e --- /dev/null +++ b/pkg/conversion/pod_example.json @@ -0,0 +1,102 @@ +{ + "kind": "Pod", + "apiVersion": "v1beta3", + "metadata": { + "name": "etcd-server-e2e-test-wojtekt-master", + "namespace": "default", + "selfLink": "/api/v1beta1/pods/etcd-server-e2e-test-wojtekt-master?namespace=default", + "uid": "a671734a-e8e5-11e4-8fde-42010af09327", + "resourceVersion": "22", + "creationTimestamp": "2015-04-22T11:49:36Z", + "annotations": { + "kubernetes.io/config.mirror": "mirror", + "kubernetes.io/config.source": "file" + } + }, + "spec": { + "volumes": [ + { + "name": "varetcd", + "hostPath": { + "path": "/mnt/master-pd/var/etcd" + }, + "emptyDir": null, + "gcePersistentDisk": null, + "awsElasticBlockStore": null, + "gitRepo": null, + "secret": null, + "nfs": null, + "iscsi": null, + "glusterfs": null + } + ], + "containers": [ + { + "name": "etcd-container", + "image": "gcr.io/google_containers/etcd:2.0.9", + "command": [ + "/usr/local/bin/etcd", + "--addr", + "127.0.0.1:4001", + "--bind-addr", + "127.0.0.1:4001", + "--data-dir", + "/var/etcd/data" + ], + "ports": [ + { + "name": "serverport", + "hostPort": 2380, + "containerPort": 2380, + "protocol": "TCP" + }, + { + "name": "clientport", + "hostPort": 4001, + "containerPort": 4001, + "protocol": "TCP" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "varetcd", + "mountPath": "/var/etcd" + } + ], + "terminationMessagePath": "/dev/termination-log", + "imagePullPolicy": "IfNotPresent", + "capabilities": {} + } + ], + "restartPolicy": "Always", + "dnsPolicy": "ClusterFirst", + "host": "e2e-test-wojtekt-master", + "hostNetwork": true + }, + "status": { + "phase": "Running", + "Condition": [ + { + "type": "Ready", + "status": "True" + } + ], + "containerStatuses": [ + { + "name": "etcd-container", + "state": { + "running": { + "startedAt": "2015-04-22T11:49:32Z" + } + }, + "lastState": {}, + "ready": true, + "restartCount": 0, + "image": "gcr.io/google_containers/etcd:2.0.9", + "imageID": "docker://b6b9a86dc06aa1361357ca1b105feba961f6a4145adca6c54e142c0be0fe87b0", + "containerID": "docker://3cbbf818f1addfc252957b4504f56ef2907a313fe6afc47fc75373674255d46d" + } + ] + } +} diff --git a/pkg/conversion/replication_controller_example.json b/pkg/conversion/replication_controller_example.json new file mode 100644 index 00000000000..8d2b3594ba9 --- /dev/null +++ b/pkg/conversion/replication_controller_example.json @@ -0,0 +1,82 @@ +{ + "kind": "ReplicationController", + "apiVersion": "v1beta3", + "metadata": { + "name": "elasticsearch-logging-controller", + "namespace": "default", + "selfLink": "/api/v1beta1/replicationControllers/elasticsearch-logging-controller?namespace=default", + "uid": "aa76f162-e8e5-11e4-8fde-42010af09327", + "resourceVersion": "98", + "creationTimestamp": "2015-04-22T11:49:43Z", + "labels": { + "kubernetes.io/cluster-service": "true", + "name": "elasticsearch-logging" + } + }, + "spec": { + "replicas": 1, + "selector": { + "name": "elasticsearch-logging" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "kubernetes.io/cluster-service": "true", + "name": "elasticsearch-logging" + } + }, + "spec": { + "volumes": [ + { + "name": "es-persistent-storage", + "hostPath": null, + "emptyDir": { + "medium": "" + }, + "gcePersistentDisk": null, + "awsElasticBlockStore": null, + "gitRepo": null, + "secret": null, + "nfs": null, + "iscsi": null, + "glusterfs": null + } + ], + "containers": [ + { + "name": "elasticsearch-logging", + "image": "gcr.io/google_containers/elasticsearch:1.0", + "ports": [ + { + "name": "es-port", + "containerPort": 9200, + "protocol": "TCP" + }, + { + "name": "es-transport-port", + "containerPort": 9300, + "protocol": "TCP" + } + ], + "resources": {}, + "volumeMounts": [ + { + "name": "es-persistent-storage", + "mountPath": "/data" + } + ], + "terminationMessagePath": "/dev/termination-log", + "imagePullPolicy": "IfNotPresent", + "capabilities": {} + } + ], + "restartPolicy": "Always", + "dnsPolicy": "ClusterFirst" + } + } + }, + "status": { + "replicas": 1 + } +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 14cb35bdc9b..41099bc9a60 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/golang/glog" @@ -479,7 +480,7 @@ func (d *NodeDescriber) Describe(namespace, name string) (string, error) { } var pods []*api.Pod - allPods, err := d.Pods(namespace).List(labels.Everything()) + allPods, err := d.Pods(namespace).List(labels.Everything(), fields.Everything()) if err != nil { return "", err } @@ -613,7 +614,7 @@ func printReplicationControllersByLabels(matchingRCs []api.ReplicationController } func getPodStatusForReplicationController(c client.PodInterface, controller *api.ReplicationController) (running, waiting, succeeded, failed int, err error) { - rcPods, err := c.List(labels.SelectorFromSet(controller.Spec.Selector)) + rcPods, err := c.List(labels.SelectorFromSet(controller.Spec.Selector), fields.Everything()) if err != nil { return } diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 958491f6e9f..d760ac86250 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -32,6 +32,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" "github.com/docker/docker/pkg/units" "github.com/ghodss/yaml" @@ -243,7 +244,8 @@ func (h *HumanReadablePrinter) HandledResources() []string { return keys } -var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED"} +var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED", "MESSAGE"} +var podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLABELS"} var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS"} var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT(S)"} var endpointColumns = []string{"NAME", "ENDPOINTS"} @@ -262,6 +264,8 @@ var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"} func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(podColumns, printPod) h.Handler(podColumns, printPodList) + h.Handler(podTemplateColumns, printPodTemplate) + h.Handler(podTemplateColumns, printPodTemplateList) h.Handler(replicationControllerColumns, printReplicationController) h.Handler(replicationControllerColumns, printReplicationControllerList) h.Handler(serviceColumns, printService) @@ -339,32 +343,92 @@ func podHostString(host, ip string) string { return host + "/" + ip } +// translateTimestamp returns the elapsed time since timestamp in +// human-readable approximation. +func translateTimestamp(timestamp util.Time) string { + return units.HumanDuration(time.Now().Sub(timestamp.Time)) +} + +// interpretContainerStatus interprets the container status and returns strings +// associated with columns "STATUS", "CREATED", and "MESSAGE". +// The meaning of MESSAGE varies based on the context of STATUS: +// STATUS: Waiting; MESSAGE: reason for waiting +// STATUS: Running; MESSAGE: reason for the last termination +// STATUS: Terminated; MESSAGE: reason for this termination +func interpretContainerStatus(status *api.ContainerStatus) (string, string, string, error) { + // Helper function to compose a meaning message from terminate state. + getTermMsg := func(state *api.ContainerStateTerminated) string { + var message string + if state != nil { + message = fmt.Sprintf("exit code %d", state.ExitCode) + if state.Reason != "" { + message = fmt.Sprintf("%s, reason: %s", state.Reason) + } + } + return message + } + + state := &status.State + if state.Waiting != nil { + return "Waiting", "", state.Waiting.Reason, nil + } else if state.Running != nil { + // Get the information of the last termination state. This is useful if + // a container is stuck in a crash loop. + message := getTermMsg(status.LastTerminationState.Termination) + if message != "" { + message = "last termination: " + message + } + return "Running", translateTimestamp(state.Running.StartedAt), message, nil + } else if state.Termination != nil { + return "Terminated", translateTimestamp(state.Termination.StartedAt), getTermMsg(state.Termination), nil + } + return "", "", "", fmt.Errorf("unknown container state %#v", *state) +} + func printPod(pod *api.Pod, w io.Writer) error { - // TODO: remove me when pods are converted - spec := &api.PodSpec{} - if err := api.Scheme.Convert(&pod.Spec, spec); err != nil { - glog.Errorf("Unable to convert pod manifest: %v", err) - } - containers := spec.Containers - var firstContainer api.Container - if len(containers) > 0 { - firstContainer, containers = containers[0], containers[1:] - } - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", pod.Name, pod.Status.PodIP, - firstContainer.Name, - firstContainer.Image, + "", "", podHostString(pod.Spec.Host, pod.Status.HostIP), formatLabels(pod.Labels), pod.Status.Phase, - units.HumanDuration(time.Now().Sub(pod.CreationTimestamp.Time))) + translateTimestamp(pod.CreationTimestamp), + pod.Status.Message, + ) if err != nil { return err } - // Lay out all the other containers on separate lines. - for _, container := range containers { - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "", "", container.Name, container.Image, "", "", "", "") + // Lay out all containers on separate lines. + statuses := pod.Status.ContainerStatuses + if len(statuses) == 0 { + // Container status has not been reported yet. Print basic information + // of the containers and exit the function. + for _, container := range pod.Spec.Containers { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + "", "", container.Name, container.Image, "", "", "", "") + if err != nil { + return err + } + } + return nil + } + + // Print the actual container statuses. + for _, status := range statuses { + state, created, message, err := interpretContainerStatus(&status) + if err != nil { + return err + } + _, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + "", "", + status.Name, + status.Image, + "", "", + state, + created, + message, + ) if err != nil { return err } @@ -381,6 +445,40 @@ func printPodList(podList *api.PodList, w io.Writer) error { return nil } +func printPodTemplate(pod *api.PodTemplate, w io.Writer) error { + containers := pod.Template.Spec.Containers + var firstContainer api.Container + if len(containers) > 0 { + firstContainer, containers = containers[0], containers[1:] + } + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", + pod.Name, + firstContainer.Name, + firstContainer.Image, + formatLabels(pod.Template.Labels), + ) + if err != nil { + return err + } + // Lay out all the other containers on separate lines. + for _, container := range containers { + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", "", container.Name, container.Image, "") + if err != nil { + return err + } + } + return nil +} + +func printPodTemplateList(podList *api.PodTemplateList, w io.Writer) error { + for _, pod := range podList.Items { + if err := printPodTemplate(&pod, w); err != nil { + return err + } + } + return nil +} + func printReplicationController(controller *api.ReplicationController, w io.Writer) error { containers := controller.Spec.Template.Spec.Containers var firstContainer api.Container diff --git a/pkg/kubelet/container/helpers.go b/pkg/kubelet/container/helpers.go new file mode 100644 index 00000000000..6776339421d --- /dev/null +++ b/pkg/kubelet/container/helpers.go @@ -0,0 +1,32 @@ +/* +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 container + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" +) + +// HandlerRunner runs a lifecycle handler for a container. +type HandlerRunner interface { + Run(containerID string, pod *api.Pod, container *api.Container, handler *api.Handler) error +} + +// Prober checks the healthiness of a container. +type Prober interface { + Probe(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) +} diff --git a/pkg/kubelet/container/runtime.go b/pkg/kubelet/container/runtime.go index 65c60f95844..c9d736d837c 100644 --- a/pkg/kubelet/container/runtime.go +++ b/pkg/kubelet/container/runtime.go @@ -25,11 +25,20 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" ) +type Version interface { + // Compare compares two versions of the runtime. On success it returns -1 + // if the version is less than the other, 1 if it is greater than the other, + // or 0 if they are equal. + Compare(other string) (int, error) + // String returns a string that represents the version. + String() string +} + // Runtime interface defines the interfaces that should be implemented // by a container runtime. type Runtime interface { - // Version returns a map of version information of the container runtime. - Version() (map[string]string, error) + // Version returns the version information of the container runtime. + Version() (Version, error) // GetPods returns a list containers group by pods. The boolean parameter // specifies whether the runtime returns all containers including those already // exited and dead containers (used for garbage collection). diff --git a/pkg/kubelet/dockertools/docker.go b/pkg/kubelet/dockertools/docker.go index 2f0a7d1c655..bd70d74b7c9 100644 --- a/pkg/kubelet/dockertools/docker.go +++ b/pkg/kubelet/dockertools/docker.go @@ -120,7 +120,8 @@ type dockerContainerCommandRunner struct { var dockerAPIVersionWithExec, _ = docker.NewAPIVersion("1.15") // Returns the major and minor version numbers of docker server. -func (d *dockerContainerCommandRunner) GetDockerServerVersion() (docker.APIVersion, error) { +// TODO(yifan): Remove this once the ContainerCommandRunner is implemented by dockerManager. +func (d *dockerContainerCommandRunner) getDockerServerVersion() (docker.APIVersion, error) { env, err := d.client.Version() if err != nil { return nil, fmt.Errorf("failed to get docker server version - %v", err) @@ -136,7 +137,7 @@ func (d *dockerContainerCommandRunner) GetDockerServerVersion() (docker.APIVersi } func (d *dockerContainerCommandRunner) nativeExecSupportExists() (bool, error) { - version, err := d.GetDockerServerVersion() + version, err := d.getDockerServerVersion() if err != nil { return false, err } @@ -295,7 +296,11 @@ func (d *dockerContainerCommandRunner) PortForward(pod *kubecontainer.Pod, port } containerPid := container.State.Pid - // TODO use exec.LookPath for socat / what if the host doesn't have it??? + // TODO what if the host doesn't have it??? + _, lookupErr := exec.LookPath("socat") + if lookupErr != nil { + return fmt.Errorf("Unable to do port forwarding: socat not found.") + } args := []string{"-t", fmt.Sprintf("%d", containerPid), "-n", "socat", "-", fmt.Sprintf("TCP4:localhost:%d", port)} // TODO use exec.LookPath command := exec.Command("nsenter", args...) @@ -479,7 +484,6 @@ func ConnectToDockerOrDie(dockerEndpoint string) DockerInterface { // TODO(yifan): Move this to container.Runtime. type ContainerCommandRunner interface { RunInContainer(containerID string, cmd []string) ([]byte, error) - GetDockerServerVersion() (docker.APIVersion, error) ExecInContainer(containerID string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error PortForward(pod *kubecontainer.Pod, port uint16, stream io.ReadWriteCloser) error } diff --git a/pkg/kubelet/dockertools/docker_test.go b/pkg/kubelet/dockertools/docker_test.go index 2b57a5820a5..514c2ceafa1 100644 --- a/pkg/kubelet/dockertools/docker_test.go +++ b/pkg/kubelet/dockertools/docker_test.go @@ -129,10 +129,10 @@ func TestContainerManifestNaming(t *testing.T) { } } -func TestGetDockerServerVersion(t *testing.T) { +func TestVersion(t *testing.T) { fakeDocker := &FakeDockerClient{VersionInfo: docker.Env{"Version=1.1.3", "ApiVersion=1.15"}} - runner := dockerContainerCommandRunner{fakeDocker} - version, err := runner.GetDockerServerVersion() + manager := &DockerManager{client: fakeDocker} + version, err := manager.Version() if err != nil { t.Errorf("got error while getting docker server version - %s", err) } diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index 4e8b15e1a28..4016bd3d0a4 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -336,6 +336,8 @@ func (dm *DockerManager) GetPodStatus(pod *api.Pod) (*api.PodStatus, error) { continue } var containerStatus api.ContainerStatus + containerStatus.Name = container.Name + containerStatus.Image = container.Image if oldStatus, found := oldStatuses[container.Name]; found { // Some states may be lost due to GC; apply the last observed // values if possible. @@ -655,3 +657,43 @@ func (dm *DockerManager) PodInfraContainerChanged(pod *api.Pod, podInfraContaine } return podInfraContainer.Hash != HashContainer(expectedPodInfraContainer), nil } + +type dockerVersion docker.APIVersion + +func NewVersion(input string) (dockerVersion, error) { + version, err := docker.NewAPIVersion(input) + return dockerVersion(version), err +} + +func (dv dockerVersion) String() string { + return docker.APIVersion(dv).String() +} + +func (dv dockerVersion) Compare(other string) (int, error) { + a := docker.APIVersion(dv) + b, err := docker.NewAPIVersion(other) + if err != nil { + return 0, err + } + if a.LessThan(b) { + return -1, nil + } + if a.GreaterThan(b) { + return 1, nil + } + return 0, nil +} + +func (dm *DockerManager) Version() (kubecontainer.Version, error) { + env, err := dm.client.Version() + if err != nil { + return nil, fmt.Errorf("docker: failed to get docker version: %v", err) + } + + apiVersion := env.Get("ApiVersion") + version, err := docker.NewAPIVersion(apiVersion) + if err != nil { + return nil, fmt.Errorf("docker: failed to parse docker server version %q: %v", apiVersion, err) + } + return dockerVersion(version), nil +} diff --git a/pkg/kubelet/handlers.go b/pkg/kubelet/handlers.go index 44b9a5045d4..be8fe96399d 100644 --- a/pkg/kubelet/handlers.go +++ b/pkg/kubelet/handlers.go @@ -22,15 +22,12 @@ import ( "strconv" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" ) -type HandlerRunner interface { - Run(containerID string, pod *api.Pod, container *api.Container, handler *api.Handler) error -} - type handlerRunner struct { httpGetter httpGetter commandRunner dockertools.ContainerCommandRunner @@ -38,7 +35,7 @@ type handlerRunner struct { } // TODO(yifan): Merge commandRunner and containerManager once containerManager implements the ContainerCommandRunner interface. -func NewHandlerRunner(httpGetter httpGetter, commandRunner dockertools.ContainerCommandRunner, containerManager *dockertools.DockerManager) *handlerRunner { +func newHandlerRunner(httpGetter httpGetter, commandRunner dockertools.ContainerCommandRunner, containerManager *dockertools.DockerManager) kubecontainer.HandlerRunner { return &handlerRunner{ httpGetter: httpGetter, commandRunner: commandRunner, diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index c58189628ab..fd061339ece 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -232,8 +232,8 @@ func NewMainKubelet( } klet.podManager = newBasicPodManager(klet.kubeClient) - klet.prober = NewProber(klet.runner, klet.readinessManager, klet.containerRefManager, klet.recorder) - klet.handlerRunner = NewHandlerRunner(klet.httpClient, klet.runner, klet.containerManager) + klet.prober = newProber(klet.runner, klet.readinessManager, klet.containerRefManager, klet.recorder) + klet.handlerRunner = newHandlerRunner(klet.httpClient, klet.runner, klet.containerManager) runtimeCache, err := kubecontainer.NewRuntimeCache(containerManager) if err != nil { @@ -317,10 +317,10 @@ type Kubelet struct { networkPlugin network.NetworkPlugin // Healthy check prober. - prober *Prober + prober kubecontainer.Prober // Container lifecycle handler runner. - handlerRunner HandlerRunner + handlerRunner kubecontainer.HandlerRunner // Container readiness state manager. readinessManager *kubecontainer.ReadinessManager @@ -1060,20 +1060,16 @@ type podContainerChangesSpec struct { containersToKeep map[dockertools.DockerID]int } -func (kl *Kubelet) computePodContainerChanges(pod *api.Pod, runningPod kubecontainer.Pod) (podContainerChangesSpec, error) { +func (kl *Kubelet) computePodContainerChanges(pod *api.Pod, runningPod kubecontainer.Pod, podStatus api.PodStatus) (podContainerChangesSpec, error) { podFullName := kubecontainer.GetPodFullName(pod) uid := pod.UID glog.V(4).Infof("Syncing Pod %+v, podFullName: %q, uid: %q", pod, podFullName, uid) - err := kl.makePodDataDirs(pod) - if err != nil { - return podContainerChangesSpec{}, err - } - containersToStart := make(map[int]empty) containersToKeep := make(map[dockertools.DockerID]int) createPodInfraContainer := false + var err error var podInfraContainerID dockertools.DockerID var changed bool podInfraContainer := runningPod.FindContainerByName(dockertools.PodInfraContainerName) @@ -1097,18 +1093,6 @@ func (kl *Kubelet) computePodContainerChanges(pod *api.Pod, runningPod kubeconta containersToKeep[podInfraContainerID] = -1 } - // Do not use the cache here since we need the newest status to check - // if we need to restart the container below. - pod, found := kl.GetPodByFullName(podFullName) - if !found { - return podContainerChangesSpec{}, fmt.Errorf("couldn't find pod %q", podFullName) - } - podStatus, err := kl.generatePodStatus(pod) - if err != nil { - glog.Errorf("Unable to get pod with name %q and uid %q info with error(%v)", podFullName, uid, err) - return podContainerChangesSpec{}, err - } - for index, container := range pod.Spec.Containers { expectedHash := dockertools.HashContainer(&container) @@ -1213,7 +1197,18 @@ func (kl *Kubelet) syncPod(pod *api.Pod, mirrorPod *api.Pod, runningPod kubecont return err } - containerChanges, err := kl.computePodContainerChanges(pod, runningPod) + if err := kl.makePodDataDirs(pod); err != nil { + glog.Errorf("Unable to make pod data directories for pod %q (uid %q): %v", podFullName, uid, err) + return err + } + + podStatus, err := kl.generatePodStatus(pod) + if err != nil { + glog.Errorf("Unable to get status for pod %q (uid %q): %v", podFullName, uid, err) + return err + } + + containerChanges, err := kl.computePodContainerChanges(pod, runningPod, podStatus) glog.V(3).Infof("Got container changes for pod %q: %+v", podFullName, containerChanges) if err != nil { return err @@ -1658,13 +1653,12 @@ func (kl *Kubelet) syncLoop(updates <-chan PodUpdate, handler SyncHandler) { } } -// Returns Docker version for this Kubelet. -func (kl *Kubelet) GetDockerVersion() (docker.APIVersion, error) { - if kl.dockerClient == nil { - return nil, fmt.Errorf("no Docker client") +// Returns the container runtime version for this Kubelet. +func (kl *Kubelet) GetContainerRuntimeVersion() (kubecontainer.Version, error) { + if kl.containerManager == nil { + return nil, fmt.Errorf("no container runtime") } - dockerRunner := dockertools.NewDockerContainerCommandRunner(kl.dockerClient) - return dockerRunner.GetDockerServerVersion() + return kl.containerManager.Version() } func (kl *Kubelet) validatePodPhase(podStatus *api.PodStatus) error { @@ -1770,6 +1764,21 @@ func (kl *Kubelet) recordNodeOnlineEvent() { kl.recorder.Eventf(kl.nodeRef, "online", "Node %s is now online", kl.hostname) } +func (kl *Kubelet) recordNodeSchedulableEvent() { + // TODO: This requires a transaction, either both node status is updated + // and event is recorded or neither should happen, see issue #6055. + kl.recorder.Eventf(kl.nodeRef, "schedulable", "Node %s is now schedulable", kl.hostname) +} + +func (kl *Kubelet) recordNodeUnschedulableEvent() { + // TODO: This requires a transaction, either both node status is updated + // and event is recorded or neither should happen, see issue #6055. + kl.recorder.Eventf(kl.nodeRef, "unschedulable", "Node %s is now unschedulable", kl.hostname) +} + +// Maintains Node.Spec.Unschedulable value from previous run of tryUpdateNodeStatus() +var oldNodeUnschedulable bool + // tryUpdateNodeStatus tries to update node status to master. func (kl *Kubelet) tryUpdateNodeStatus() error { node, err := kl.kubeClient.Nodes().Get(kl.hostname) @@ -1836,6 +1845,14 @@ func (kl *Kubelet) tryUpdateNodeStatus() error { kl.recordNodeOnlineEvent() } + if oldNodeUnschedulable != node.Spec.Unschedulable { + if node.Spec.Unschedulable { + kl.recordNodeUnschedulableEvent() + } else { + kl.recordNodeSchedulableEvent() + } + oldNodeUnschedulable = node.Spec.Unschedulable + } _, err = kl.kubeClient.Nodes().UpdateStatus(node) return err } diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 5fc8e740410..899bad7971d 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -113,8 +113,8 @@ func newTestKubelet(t *testing.T) *TestKubelet { }, fakeRecorder) kubelet.containerManager.Puller = &dockertools.FakeDockerPuller{} - kubelet.prober = NewProber(nil, kubelet.readinessManager, kubelet.containerRefManager, kubelet.recorder) - kubelet.handlerRunner = NewHandlerRunner(&fakeHTTP{}, &fakeContainerCommandRunner{}, kubelet.containerManager) + kubelet.prober = newProber(nil, kubelet.readinessManager, kubelet.containerRefManager, kubelet.recorder) + kubelet.handlerRunner = newHandlerRunner(&fakeHTTP{}, &fakeContainerCommandRunner{}, kubelet.containerManager) return &TestKubelet{kubelet, fakeDocker, mockCadvisor, fakeKubeClient, waitGroup, fakeMirrorClient} } @@ -511,10 +511,10 @@ func TestSyncPodsDoesNothing(t *testing.T) { waitGroup.Wait() verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra contianer. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", + // Check the pod infra contianer. + "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container"}) } @@ -743,10 +743,10 @@ func TestSyncPodsWithPodInfraCreatesContainer(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_image", + // Check the pod infra container. + "inspect_container", // Create container. "create", "start", // Get pod status. @@ -768,7 +768,7 @@ func TestSyncPodsWithPodInfraCreatesContainerCallsHandler(t *testing.T) { waitGroup := testKubelet.waitGroup fakeHttp := fakeHTTP{} kubelet.httpClient = &fakeHttp - kubelet.handlerRunner = NewHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) + kubelet.handlerRunner = newHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) pods := []*api.Pod{ { ObjectMeta: api.ObjectMeta{ @@ -818,10 +818,10 @@ func TestSyncPodsWithPodInfraCreatesContainerCallsHandler(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_image", + // Check the pod infra container. + "inspect_container", // Create container. "create", "start", // Get pod status. @@ -1104,10 +1104,10 @@ func TestSyncPodsDeletesDuplicate(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Kill the duplicated container. "stop", // Get pod status. @@ -1175,10 +1175,10 @@ func TestSyncPodsBadHash(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Kill and restart the bad hash container. "stop", "create", "start", // Get pod status. @@ -1249,10 +1249,10 @@ func TestSyncPodsUnhealthy(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Kill the unhealthy container. "stop", // Restart the unhealthy container. @@ -1597,10 +1597,6 @@ func (f *fakeContainerCommandRunner) RunInContainer(id string, cmd []string) ([] return []byte{}, f.E } -func (f *fakeContainerCommandRunner) GetDockerServerVersion() (docker.APIVersion, error) { - return nil, nil -} - func (f *fakeContainerCommandRunner) ExecInContainer(id string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error { f.Cmd = cmd f.ID = id @@ -1694,7 +1690,7 @@ func TestRunHandlerExec(t *testing.T) { kubelet := testKubelet.kubelet fakeDocker := testKubelet.fakeDocker kubelet.runner = &fakeCommandRunner - kubelet.handlerRunner = NewHandlerRunner(&fakeHTTP{}, kubelet.runner, kubelet.containerManager) + kubelet.handlerRunner = newHandlerRunner(&fakeHTTP{}, kubelet.runner, kubelet.containerManager) containerID := "abc1234" podName := "podFoo" @@ -1749,7 +1745,7 @@ func TestRunHandlerHttp(t *testing.T) { testKubelet := newTestKubelet(t) kubelet := testKubelet.kubelet kubelet.httpClient = &fakeHttp - kubelet.handlerRunner = NewHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) + kubelet.handlerRunner = newHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) containerID := "abc1234" podName := "podFoo" @@ -1817,7 +1813,7 @@ func TestSyncPodEventHandlerFails(t *testing.T) { kubelet.httpClient = &fakeHTTP{ err: fmt.Errorf("test error"), } - kubelet.handlerRunner = NewHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) + kubelet.handlerRunner = newHandlerRunner(kubelet.httpClient, &fakeContainerCommandRunner{}, kubelet.containerManager) pods := []*api.Pod{ { @@ -1868,10 +1864,10 @@ func TestSyncPodEventHandlerFails(t *testing.T) { verifyCalls(t, fakeDocker, []string{ "list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_image", + // Check the pod infra container. + "inspect_container", // Create the container. "create", "start", // Kill the container since event handler fails. @@ -3871,10 +3867,10 @@ func TestSyncPodsWithRestartPolicy(t *testing.T) { { api.RestartPolicyAlways, []string{"list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Restart both containers. "create", "start", "create", "start", // Get pod status. @@ -3885,10 +3881,10 @@ func TestSyncPodsWithRestartPolicy(t *testing.T) { { api.RestartPolicyOnFailure, []string{"list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Restart the failed container. "create", "start", // Get pod status. @@ -3899,10 +3895,10 @@ func TestSyncPodsWithRestartPolicy(t *testing.T) { { api.RestartPolicyNever, []string{"list", "list", - // Check the pod infra container. - "inspect_container", // Get pod status. "list", "inspect_container", "inspect_container", "inspect_container", + // Check the pod infra container. + "inspect_container", // Stop the last pod infra container. "stop", // Get pod status. diff --git a/pkg/kubelet/probe.go b/pkg/kubelet/probe.go index cc8ac163dbb..7c3bf235128 100644 --- a/pkg/kubelet/probe.go +++ b/pkg/kubelet/probe.go @@ -37,9 +37,8 @@ import ( const maxProbeRetries = 3 -// Prober helps to check the liveness/readiness of a container. -// TODO(yifan): Replace the concrete type with interface later. -type Prober struct { +// prober helps to check the liveness/readiness of a container. +type prober struct { exec execprobe.ExecProber http httprobe.HTTPProber tcp tcprobe.TCPProber @@ -52,13 +51,13 @@ type Prober struct { // NewProber creates a Prober, it takes a command runner and // several container info managers. -func NewProber( +func newProber( runner dockertools.ContainerCommandRunner, readinessManager *kubecontainer.ReadinessManager, refManager *kubecontainer.RefManager, - recorder record.EventRecorder) *Prober { + recorder record.EventRecorder) kubecontainer.Prober { - return &Prober{ + return &prober{ exec: execprobe.New(), http: httprobe.New(), tcp: tcprobe.New(), @@ -73,7 +72,7 @@ func NewProber( // Probe checks the liveness/readiness of the given container. // If the container's liveness probe is unsuccessful, set readiness to false. // If liveness is successful, do a readiness check and set readiness accordingly. -func (pb *Prober) Probe(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { +func (pb *prober) Probe(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { // Probe liveness. live, err := pb.probeLiveness(pod, status, container, containerID, createdAt) if err != nil { @@ -113,7 +112,7 @@ func (pb *Prober) Probe(pod *api.Pod, status api.PodStatus, container api.Contai // probeLiveness probes the liveness of a container. // If the initalDelay since container creation on liveness probe has not passed the probe will return probe.Success. -func (pb *Prober) probeLiveness(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { +func (pb *prober) probeLiveness(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { p := container.LivenessProbe if p == nil { return probe.Success, nil @@ -126,7 +125,7 @@ func (pb *Prober) probeLiveness(pod *api.Pod, status api.PodStatus, container ap // probeReadiness probes the readiness of a container. // If the initial delay on the readiness probe has not passed the probe will return probe.Failure. -func (pb *Prober) probeReadiness(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { +func (pb *prober) probeReadiness(pod *api.Pod, status api.PodStatus, container api.Container, containerID string, createdAt int64) (probe.Result, error) { p := container.ReadinessProbe if p == nil { return probe.Success, nil @@ -139,7 +138,7 @@ func (pb *Prober) probeReadiness(pod *api.Pod, status api.PodStatus, container a // runProbeWithRetries tries to probe the container in a finite loop, it returns the last result // if it never succeeds. -func (pb *Prober) runProbeWithRetries(p *api.Probe, pod *api.Pod, status api.PodStatus, container api.Container, containerID string, retires int) (probe.Result, error) { +func (pb *prober) runProbeWithRetries(p *api.Probe, pod *api.Pod, status api.PodStatus, container api.Container, containerID string, retires int) (probe.Result, error) { var err error var result probe.Result for i := 0; i < retires; i++ { @@ -151,7 +150,7 @@ func (pb *Prober) runProbeWithRetries(p *api.Probe, pod *api.Pod, status api.Pod return result, err } -func (pb *Prober) runProbe(p *api.Probe, pod *api.Pod, status api.PodStatus, container api.Container, containerID string) (probe.Result, error) { +func (pb *prober) runProbe(p *api.Probe, pod *api.Pod, status api.PodStatus, container api.Container, containerID string) (probe.Result, error) { timeout := time.Duration(p.TimeoutSeconds) * time.Second if p.Exec != nil { glog.V(4).Infof("Exec-Probe Pod: %v, Container: %v", pod, container) @@ -228,7 +227,7 @@ type execInContainer struct { run func() ([]byte, error) } -func (p *Prober) newExecInContainer(pod *api.Pod, container api.Container, containerID string) exec.Cmd { +func (p *prober) newExecInContainer(pod *api.Pod, container api.Container, containerID string) exec.Cmd { return execInContainer{func() ([]byte, error) { return p.runner.RunInContainer(containerID, container.LivenessProbe.Exec.Command) }} diff --git a/pkg/kubelet/probe_test.go b/pkg/kubelet/probe_test.go index b87878cb391..c0f3fe47763 100644 --- a/pkg/kubelet/probe_test.go +++ b/pkg/kubelet/probe_test.go @@ -152,7 +152,7 @@ func makeTestKubelet(result probe.Result, err error) *Kubelet { containerRefManager: kubecontainer.NewRefManager(), } - kl.prober = &Prober{ + kl.prober = &prober{ exec: fakeExecProber{ result: result, err: err, diff --git a/pkg/kubelet/server.go b/pkg/kubelet/server.go index 4b98f0232cb..e261e0ff8c3 100644 --- a/pkg/kubelet/server.go +++ b/pkg/kubelet/server.go @@ -41,7 +41,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util/flushwriter" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream/spdy" - "github.com/fsouza/go-dockerclient" "github.com/golang/glog" cadvisorApi "github.com/google/cadvisor/info/v1" "github.com/prometheus/client_golang/prometheus" @@ -101,7 +100,7 @@ func ListenAndServeKubeletReadOnlyServer(host HostInterface, address net.IP, por type HostInterface interface { GetContainerInfo(podFullName string, uid types.UID, containerName string, req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) GetRootInfo(req *cadvisorApi.ContainerInfoRequest) (*cadvisorApi.ContainerInfo, error) - GetDockerVersion() (docker.APIVersion, error) + GetContainerRuntimeVersion() (kubecontainer.Version, error) GetCachedMachineInfo() (*cadvisorApi.MachineInfo, error) GetPods() []*api.Pod GetPodByName(namespace, name string) (*api.Pod, bool) @@ -160,18 +159,18 @@ func (s *Server) error(w http.ResponseWriter, err error) { http.Error(w, msg, http.StatusInternalServerError) } -func isValidDockerVersion(ver docker.APIVersion) bool { - minAllowedVersion, _ := docker.NewAPIVersion("1.15") - return ver.GreaterThanOrEqualTo(minAllowedVersion) -} - func (s *Server) dockerHealthCheck(req *http.Request) error { - version, err := s.host.GetDockerVersion() + version, err := s.host.GetContainerRuntimeVersion() if err != nil { return errors.New("unknown Docker version") } - if !isValidDockerVersion(version) { - return fmt.Errorf("Docker version is too old (%v)", version.String()) + // Verify the docker version. + result, err := version.Compare("1.15") + if err != nil { + return err + } + if result < 0 { + return fmt.Errorf("Docker version is too old: %q", version.String()) } return nil } diff --git a/pkg/kubelet/server_test.go b/pkg/kubelet/server_test.go index 712ffdd9b2b..778cf720346 100644 --- a/pkg/kubelet/server_test.go +++ b/pkg/kubelet/server_test.go @@ -32,10 +32,11 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/httpstream/spdy" - "github.com/fsouza/go-dockerclient" cadvisorApi "github.com/google/cadvisor/info/v1" ) @@ -48,7 +49,7 @@ type fakeKubelet struct { podsFunc func() []*api.Pod logFunc func(w http.ResponseWriter, req *http.Request) runFunc func(podFullName string, uid types.UID, containerName string, cmd []string) ([]byte, error) - dockerVersionFunc func() (docker.APIVersion, error) + containerVersionFunc func() (kubecontainer.Version, error) execFunc func(pod string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error portForwardFunc func(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error containerLogsFunc func(podFullName, containerName, tail string, follow bool, stdout, stderr io.Writer) error @@ -72,8 +73,8 @@ func (fk *fakeKubelet) GetRootInfo(req *cadvisorApi.ContainerInfoRequest) (*cadv return fk.rootInfoFunc(req) } -func (fk *fakeKubelet) GetDockerVersion() (docker.APIVersion, error) { - return fk.dockerVersionFunc() +func (fk *fakeKubelet) GetContainerRuntimeVersion() (kubecontainer.Version, error) { + return fk.containerVersionFunc() } func (fk *fakeKubelet) GetCachedMachineInfo() (*cadvisorApi.MachineInfo, error) { @@ -450,8 +451,8 @@ func TestPodsInfo(t *testing.T) { func TestHealthCheck(t *testing.T) { fw := newServerTest() - fw.fakeKubelet.dockerVersionFunc = func() (docker.APIVersion, error) { - return docker.NewAPIVersion("1.15") + fw.fakeKubelet.containerVersionFunc = func() (kubecontainer.Version, error) { + return dockertools.NewVersion("1.15") } fw.fakeKubelet.hostnameFunc = func() string { return "127.0.0.1" @@ -489,9 +490,9 @@ func TestHealthCheck(t *testing.T) { t.Errorf("expected status code %d, got %d", http.StatusOK, resp.StatusCode) } - //Test with old docker version - fw.fakeKubelet.dockerVersionFunc = func() (docker.APIVersion, error) { - return docker.NewAPIVersion("1.1") + //Test with old container runtime version + fw.fakeKubelet.containerVersionFunc = func() (kubecontainer.Version, error) { + return dockertools.NewVersion("1.1") } resp, err = http.Get(fw.testHTTPServer.URL + "/healthz") diff --git a/pkg/master/master.go b/pkg/master/master.go index 0b4c582f978..b8440e52d03 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -58,6 +58,7 @@ import ( pvcetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/persistentvolumeclaim/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" podetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod/etcd" + podtemplateetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/podtemplate/etcd" resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" @@ -360,9 +361,18 @@ func logStackOnRecover(panicReason interface{}, httpWriter http.ResponseWriter) // init initializes master. func (m *Master) init(c *Config) { + // TODO: make initialization of the helper part of the Master, and allow some storage + // objects to have a newer storage version than the user's default. + newerHelper, err := NewEtcdHelper(c.EtcdHelper.Client, "v1beta3") + if err != nil { + glog.Fatalf("Unable to setup storage for v1beta3: %v", err) + } + podStorage := podetcd.NewStorage(c.EtcdHelper, c.KubeletClient) podRegistry := pod.NewRegistry(podStorage.Pod) + podTemplateStorage := podtemplateetcd.NewREST(newerHelper) + eventRegistry := event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())) limitRangeRegistry := limitrange.NewEtcdRegistry(c.EtcdHelper) @@ -397,6 +407,8 @@ func (m *Master) init(c *Config) { "pods/binding": podStorage.Binding, "bindings": podStorage.Binding, + "podTemplates": podTemplateStorage, + "replicationControllers": controllerStorage, "services": service.NewStorage(m.serviceRegistry, m.nodeRegistry, m.endpointRegistry, m.portalNet, c.ClusterName), "endpoints": endpointsStorage, @@ -606,6 +618,9 @@ func (m *Master) defaultAPIGroupVersion() *apiserver.APIGroupVersion { func (m *Master) api_v1beta1() *apiserver.APIGroupVersion { storage := make(map[string]rest.Storage) for k, v := range m.storage { + if k == "podTemplates" { + continue + } storage[k] = v } version := m.defaultAPIGroupVersion() @@ -619,6 +634,9 @@ func (m *Master) api_v1beta1() *apiserver.APIGroupVersion { func (m *Master) api_v1beta2() *apiserver.APIGroupVersion { storage := make(map[string]rest.Storage) for k, v := range m.storage { + if k == "podTemplates" { + continue + } storage[k] = v } version := m.defaultAPIGroupVersion() diff --git a/pkg/namespace/namespace_controller.go b/pkg/namespace/namespace_controller.go index cf8412d5649..c24950089cd 100644 --- a/pkg/namespace/namespace_controller.go +++ b/pkg/namespace/namespace_controller.go @@ -246,7 +246,7 @@ func deleteReplicationControllers(kubeClient client.Interface, ns string) error } func deletePods(kubeClient client.Interface, ns string) error { - items, err := kubeClient.Pods(ns).List(labels.Everything()) + items, err := kubeClient.Pods(ns).List(labels.Everything(), fields.Everything()) if err != nil { return err } diff --git a/pkg/registry/controller/etcd/etcd_test.go b/pkg/registry/controller/etcd/etcd_test.go index f1f3b48bd30..c6c0d630294 100644 --- a/pkg/registry/controller/etcd/etcd_test.go +++ b/pkg/registry/controller/etcd/etcd_test.go @@ -60,7 +60,7 @@ func createController(storage *REST, rc api.ReplicationController, t *testing.T) } var validPodTemplate = api.PodTemplate{ - Spec: api.PodTemplateSpec{ + Template: api.PodTemplateSpec{ ObjectMeta: api.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, @@ -79,8 +79,8 @@ var validPodTemplate = api.PodTemplate{ } var validControllerSpec = api.ReplicationControllerSpec{ - Selector: validPodTemplate.Spec.Labels, - Template: &validPodTemplate.Spec, + Selector: validPodTemplate.Template.Labels, + Template: &validPodTemplate.Template, } var validController = api.ReplicationController{ @@ -161,7 +161,7 @@ func TestCreateControllerWithGeneratedName(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: map[string]string{"a": "b"}, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, } @@ -663,7 +663,7 @@ func TestCreate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: map[string]string{"a": "b"}, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, // invalid @@ -671,7 +671,7 @@ func TestCreate(t *testing.T) { Spec: api.ReplicationControllerSpec{ Replicas: 2, Selector: map[string]string{}, - Template: &validPodTemplate.Spec, + Template: &validPodTemplate.Template, }, }, ) diff --git a/pkg/registry/pod/rest.go b/pkg/registry/pod/rest.go index 33738073cd8..1cab2036331 100644 --- a/pkg/registry/pod/rest.go +++ b/pkg/registry/pod/rest.go @@ -35,7 +35,6 @@ import ( ) // podStrategy implements behavior for Pods -// TODO: move to a pod specific package. type podStrategy struct { runtime.ObjectTyper api.NameGenerator diff --git a/pkg/registry/podtemplate/doc.go b/pkg/registry/podtemplate/doc.go new file mode 100644 index 00000000000..3be9642c4d0 --- /dev/null +++ b/pkg/registry/podtemplate/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 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 podtemplate provides RESTStorage implementations for storing PodTemplate API objects. +package podtemplate diff --git a/pkg/registry/podtemplate/etcd/etcd.go b/pkg/registry/podtemplate/etcd/etcd.go new file mode 100644 index 00000000000..421ddb8cbb9 --- /dev/null +++ b/pkg/registry/podtemplate/etcd/etcd.go @@ -0,0 +1,63 @@ +/* +Copyright 2014 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 etcd + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/podtemplate" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// rest implements a RESTStorage for pod templates against etcd +type REST struct { + etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against pod templates. +func NewREST(h tools.EtcdHelper) *REST { + prefix := "/registry/podtemplates" + store := etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.PodTemplate{} }, + NewListFunc: func() runtime.Object { return &api.PodTemplateList{} }, + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.PodTemplate).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return podtemplate.MatchPodTemplate(label, field) + }, + EndpointName: "podtemplates", + + CreateStrategy: podtemplate.Strategy, + UpdateStrategy: podtemplate.Strategy, + ReturnDeletedObject: true, + + Helper: h, + } + + return &REST{store} +} diff --git a/pkg/registry/podtemplate/etcd/etcd_test.go b/pkg/registry/podtemplate/etcd/etcd_test.go new file mode 100644 index 00000000000..9400032331c --- /dev/null +++ b/pkg/registry/podtemplate/etcd/etcd_test.go @@ -0,0 +1,99 @@ +/* +Copyright 2014 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 etcd + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.NewEtcdHelper(fakeEtcdClient, v1beta3.Codec) + return fakeEtcdClient, helper +} + +func validNewPodTemplate(name string) *api.PodTemplate { + return &api.PodTemplate{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: api.NamespaceDefault, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"test": "foo"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{ + { + Name: "foo", + Image: "test", + ImagePullPolicy: api.PullAlways, + + TerminationMessagePath: api.TerminationMessagePathDefault, + }, + }, + }, + }, + } +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + pod := validNewPodTemplate("foo") + pod.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + pod, + // invalid + &api.PodTemplate{ + Template: api.PodTemplateSpec{}, + }, + ) +} + +func TestUpdate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + + fakeEtcdClient.ExpectNotFoundGet("/registry/podtemplates/default/foo") + fakeEtcdClient.ChangeIndex = 2 + pod := validNewPodTemplate("foo") + existing := validNewPodTemplate("exists") + obj, err := storage.Create(api.NewDefaultContext(), existing) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + older := obj.(*api.PodTemplate) + older.ResourceVersion = "1" + + test.TestUpdate( + pod, + existing, + older, + ) +} diff --git a/pkg/registry/podtemplate/rest.go b/pkg/registry/podtemplate/rest.go new file mode 100644 index 00000000000..9204c098e75 --- /dev/null +++ b/pkg/registry/podtemplate/rest.go @@ -0,0 +1,81 @@ +/* +Copyright 2014 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 podtemplate + +import ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + errs "github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors" +) + +// podTemplateStrategy implements behavior for PodTemplates +type podTemplateStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating PodTemplate +// objects via the REST API. +var Strategy = podTemplateStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is true for pod templates. +func (podTemplateStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears fields that are not allowed to be set by end users on creation. +func (podTemplateStrategy) PrepareForCreate(obj runtime.Object) { + _ = obj.(*api.PodTemplate) +} + +// Validate validates a new pod template. +func (podTemplateStrategy) Validate(ctx api.Context, obj runtime.Object) errs.ValidationErrorList { + pod := obj.(*api.PodTemplate) + return validation.ValidatePodTemplate(pod) +} + +// AllowCreateOnUpdate is false for pod templates. +func (podTemplateStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (podTemplateStrategy) PrepareForUpdate(obj, old runtime.Object) { + _ = obj.(*api.PodTemplate) +} + +// ValidateUpdate is the default update validation for an end user. +func (podTemplateStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) errs.ValidationErrorList { + return validation.ValidatePodTemplateUpdate(obj.(*api.PodTemplate), old.(*api.PodTemplate)) +} + +// MatchPodTemplate returns a generic matcher for a given label and field selector. +func MatchPodTemplate(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + podObj, ok := obj.(*api.PodTemplate) + if !ok { + return false, fmt.Errorf("not a pod template") + } + return label.Matches(labels.Set(podObj.Labels)), nil + }) +} diff --git a/pkg/resourcequota/resource_quota_controller.go b/pkg/resourcequota/resource_quota_controller.go index 8998773bb15..be63be334de 100644 --- a/pkg/resourcequota/resource_quota_controller.go +++ b/pkg/resourcequota/resource_quota_controller.go @@ -148,7 +148,7 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err pods := &api.PodList{} if set[api.ResourcePods] || set[api.ResourceMemory] || set[api.ResourceCPU] { - pods, err = rm.kubeClient.Pods(usage.Namespace).List(labels.Everything()) + pods, err = rm.kubeClient.Pods(usage.Namespace).List(labels.Everything(), fields.Everything()) if err != nil { return err } @@ -240,6 +240,28 @@ func PodCPU(pod *api.Pod) *resource.Quantity { return resource.NewMilliQuantity(int64(val), resource.DecimalSI) } +// IsPodCPUUnbounded returns true if the cpu use is unbounded for any container in pod +func IsPodCPUUnbounded(pod *api.Pod) bool { + for j := range pod.Spec.Containers { + container := pod.Spec.Containers[j] + if container.Resources.Limits.Cpu().MilliValue() == int64(0) { + return true + } + } + return false +} + +// IsPodMemoryUnbounded returns true if the memory use is unbounded for any container in pod +func IsPodMemoryUnbounded(pod *api.Pod) bool { + for j := range pod.Spec.Containers { + container := pod.Spec.Containers[j] + if container.Resources.Limits.Memory().Value() == int64(0) { + return true + } + } + return false +} + // PodMemory computes the memory usage of a pod func PodMemory(pod *api.Pod) *resource.Quantity { val := int64(0) diff --git a/pkg/resourcequota/resource_quota_controller_test.go b/pkg/resourcequota/resource_quota_controller_test.go index 8d03ab44c34..2f112ccbf02 100644 --- a/pkg/resourcequota/resource_quota_controller_test.go +++ b/pkg/resourcequota/resource_quota_controller_test.go @@ -267,3 +267,63 @@ func TestSyncResourceQuotaNoChange(t *testing.T) { t.Errorf("SyncResourceQuota made an unexpected client action when state was not dirty: %v", kubeClient.Actions) } } + +func TestIsPodCPUUnbounded(t *testing.T) { + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "pod-running"}, + Status: api.PodStatus{Phase: api.PodRunning}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "0")}}, + }, + } + if IsPodCPUUnbounded(&pod) { + t.Errorf("Expected false") + } + pod = api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "pod-running"}, + Status: api.PodStatus{Phase: api.PodRunning}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("0", "0")}}, + }, + } + if !IsPodCPUUnbounded(&pod) { + t.Errorf("Expected true") + } + + pod.Spec.Containers[0].Resources = api.ResourceRequirements{} + if !IsPodCPUUnbounded(&pod) { + t.Errorf("Expected true") + } +} + +func TestIsPodMemoryUnbounded(t *testing.T) { + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "pod-running"}, + Status: api.PodStatus{Phase: api.PodRunning}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("0", "1Gi")}}, + }, + } + if IsPodMemoryUnbounded(&pod) { + t.Errorf("Expected false") + } + pod = api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "pod-running"}, + Status: api.PodStatus{Phase: api.PodRunning}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("0", "0")}}, + }, + } + if !IsPodMemoryUnbounded(&pod) { + t.Errorf("Expected true") + } + + pod.Spec.Containers[0].Resources = api.ResourceRequirements{} + if !IsPodMemoryUnbounded(&pod) { + t.Errorf("Expected true") + } +} diff --git a/pkg/runtime/scheme.go b/pkg/runtime/scheme.go index b8047c7dc3c..2670c0946e0 100644 --- a/pkg/runtime/scheme.go +++ b/pkg/runtime/scheme.go @@ -36,6 +36,10 @@ type Scheme struct { // Function to convert a field selector to internal representation. type FieldLabelConversionFunc func(label, value string) (internalLabel, internalValue string, err error) +func (self *Scheme) Raw() *conversion.Scheme { + return self.raw +} + // fromScope gets the input version, desired output version, and desired Scheme // from a conversion.Scope. func (self *Scheme) fromScope(s conversion.Scope) (inVersion, outVersion string, scheme *Scheme) { diff --git a/pkg/service/endpoints_controller.go b/pkg/service/endpoints_controller.go index b5c0f317998..91548a8da8a 100644 --- a/pkg/service/endpoints_controller.go +++ b/pkg/service/endpoints_controller.go @@ -85,7 +85,7 @@ func NewEndpointController(client *client.Client) *EndpointController { e.podStore.Store, e.podController = framework.NewInformer( &cache.ListWatch{ ListFunc: func() (runtime.Object, error) { - return e.client.Pods(api.NamespaceAll).List(labels.Everything()) + return e.client.Pods(api.NamespaceAll).List(labels.Everything(), fields.Everything()) }, WatchFunc: func(rv string) (watch.Interface, error) { return e.client.Pods(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), rv) diff --git a/pkg/volume/iscsi/iscsi_test.go b/pkg/volume/iscsi/iscsi_test.go index 8b3b6cc646b..bc1396de945 100644 --- a/pkg/volume/iscsi/iscsi_test.go +++ b/pkg/volume/iscsi/iscsi_test.go @@ -39,7 +39,10 @@ func TestCanSupport(t *testing.T) { } } -type fakeDiskManager struct{} +type fakeDiskManager struct { + attachCalled bool + detachCalled bool +} func (fake *fakeDiskManager) MakeGlobalPDName(disk iscsiDisk) string { return "/tmp/fake_iscsi_path" @@ -50,6 +53,11 @@ func (fake *fakeDiskManager) AttachDisk(disk iscsiDisk) error { if err != nil { return err } + // Simulate the global mount so that the fakeMounter returns the + // expected number of mounts for the attached disk. + disk.mounter.Mount(globalPath, globalPath, disk.fsType, 0, "") + + fake.attachCalled = true return nil } @@ -59,6 +67,7 @@ func (fake *fakeDiskManager) DetachDisk(disk iscsiDisk, mntPath string) error { if err != nil { return err } + fake.detachCalled = true return nil } @@ -81,7 +90,9 @@ func TestPlugin(t *testing.T) { }, }, } - builder, err := plug.(*ISCSIPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), types.UID("poduid"), &fakeDiskManager{}, &mount.FakeMounter{}) + fakeManager := &fakeDiskManager{} + fakeMounter := &mount.FakeMounter{} + builder, err := plug.(*ISCSIPlugin).newBuilderInternal(volume.NewSpecFromVolume(spec), types.UID("poduid"), fakeManager, fakeMounter) if err != nil { t.Errorf("Failed to make a new Builder: %v", err) } @@ -111,8 +122,12 @@ func TestPlugin(t *testing.T) { t.Errorf("SetUp() failed: %v", err) } } + if !fakeManager.attachCalled { + t.Errorf("Attach was not called") + } - cleaner, err := plug.(*ISCSIPlugin).newCleanerInternal("vol1", types.UID("poduid"), &fakeDiskManager{}, &mount.FakeMounter{}) + fakeManager = &fakeDiskManager{} + cleaner, err := plug.(*ISCSIPlugin).newCleanerInternal("vol1", types.UID("poduid"), fakeManager, fakeMounter) if err != nil { t.Errorf("Failed to make a new Cleaner: %v", err) } @@ -128,4 +143,7 @@ func TestPlugin(t *testing.T) { } else if !os.IsNotExist(err) { t.Errorf("SetUp() failed: %v", err) } + if !fakeManager.detachCalled { + t.Errorf("Detach was not called") + } } diff --git a/plugin/cmd/kube-scheduler/app/server.go b/plugin/cmd/kube-scheduler/app/server.go index c8696ee1173..9366e5a12ad 100644 --- a/plugin/cmd/kube-scheduler/app/server.go +++ b/plugin/cmd/kube-scheduler/app/server.go @@ -70,7 +70,7 @@ func NewSchedulerServer() *SchedulerServer { func (s *SchedulerServer) AddFlags(fs *pflag.FlagSet) { fs.IntVar(&s.Port, "port", s.Port, "The port that the scheduler's http service runs on") fs.Var(&s.Address, "address", "The IP address to serve on (set to 0.0.0.0 for all interfaces)") - fs.StringVar(&s.AlgorithmProvider, "algorithm_provider", s.AlgorithmProvider, "The scheduling algorithm provider to use") + fs.StringVar(&s.AlgorithmProvider, "algorithm_provider", s.AlgorithmProvider, "The scheduling algorithm provider to use, one of: "+factory.ListAlgorithmProviders()) fs.StringVar(&s.PolicyConfigFile, "policy_config_file", s.PolicyConfigFile, "File with scheduler policy configuration") fs.BoolVar(&s.EnableProfiling, "profiling", true, "Enable profiling via web interface host:port/debug/pprof/") fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go index 2b2808df693..0def71e8407 100644 --- a/plugin/pkg/admission/resourcequota/admission.go +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -171,6 +171,9 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli hardMem, hardMemFound := status.Hard[api.ResourceMemory] if hardMemFound { + if set[api.ResourceMemory] && resourcequota.IsPodMemoryUnbounded(pod) { + return false, fmt.Errorf("Limited to %s memory, but pod has no specified memory limit", hardMem.String()) + } used, usedFound := status.Used[api.ResourceMemory] if !usedFound { return false, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.") @@ -184,6 +187,9 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli } hardCPU, hardCPUFound := status.Hard[api.ResourceCPU] if hardCPUFound { + if set[api.ResourceCPU] && resourcequota.IsPodCPUUnbounded(pod) { + return false, fmt.Errorf("Limited to %s CPU, but pod has no specified cpu limit", hardCPU.String()) + } used, usedFound := status.Used[api.ResourceCPU] if !usedFound { return false, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.") diff --git a/plugin/pkg/admission/resourcequota/admission_test.go b/plugin/pkg/admission/resourcequota/admission_test.go index 29265fc6867..48a6c6b2898 100644 --- a/plugin/pkg/admission/resourcequota/admission_test.go +++ b/plugin/pkg/admission/resourcequota/admission_test.go @@ -195,6 +195,72 @@ func TestIncrementUsageCPU(t *testing.T) { } } +func TestUnboundedCPU(t *testing.T) { + namespace := "default" + client := testclient.NewSimpleFake(&api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }) + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceCPU + status.Hard[r] = resource.MustParse("200m") + status.Used[r] = resource.MustParse("100m") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("0m", "1Gi")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, "Pod", namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected CPU unbounded usage error") + } +} + +func TestUnboundedMemory(t *testing.T) { + namespace := "default" + client := testclient.NewSimpleFake(&api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }) + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceMemory + status.Hard[r] = resource.MustParse("10Gi") + status.Used[r] = resource.MustParse("1Gi") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("250m", "0")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, "Pod", namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected memory unbounded usage error") + } +} + func TestExceedUsageCPU(t *testing.T) { namespace := "default" client := testclient.NewSimpleFake(&api.PodList{ diff --git a/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go b/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go index 2dffdeb0e0f..98f4d173264 100644 --- a/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go +++ b/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go @@ -60,6 +60,8 @@ func defaultPriorities() util.StringSet { return util.NewStringSet( // Prioritize nodes by least requested utilization. factory.RegisterPriorityFunction("LeastRequestedPriority", algorithm.LeastRequestedPriority, 1), + // Prioritizes nodes to help achieve balanced resource usage + factory.RegisterPriorityFunction("BalancedResourceAllocation", algorithm.BalancedResourceAllocation, 1), // spreads pods by minimizing the number of pods (belonging to the same service) on the same minion. factory.RegisterPriorityConfigFactory( "ServiceSpreadingPriority", diff --git a/plugin/pkg/scheduler/factory/plugins.go b/plugin/pkg/scheduler/factory/plugins.go index a8737f5723f..0e44f2b81ce 100644 --- a/plugin/pkg/scheduler/factory/plugins.go +++ b/plugin/pkg/scheduler/factory/plugins.go @@ -19,6 +19,7 @@ package factory import ( "fmt" "regexp" + "strings" "sync" algorithm "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler" @@ -300,3 +301,12 @@ func validatePriorityOrDie(priority schedulerapi.PriorityPolicy) { } } } + +// ListAlgorithmProviders is called when listing all available algortihm providers in `kube-scheduler --help` +func ListAlgorithmProviders() string { + var availableAlgorithmProviders []string + for name := range algorithmProviderMap { + availableAlgorithmProviders = append(availableAlgorithmProviders, name) + } + return strings.Join(availableAlgorithmProviders, " | ") +} diff --git a/test/e2e/density.go b/test/e2e/density.go index 8620fd98503..d25c5fe2fdb 100644 --- a/test/e2e/density.go +++ b/test/e2e/density.go @@ -35,14 +35,14 @@ import ( ) // Convenient wrapper around listing pods supporting retries. -func listPods(c *client.Client, namespace string, label labels.Selector) (*api.PodList, error) { +func listPods(c *client.Client, namespace string, label labels.Selector, field fields.Selector) (*api.PodList, error) { maxRetries := 4 - pods, err := c.Pods(namespace).List(label) + pods, err := c.Pods(namespace).List(label, field) for i := 0; i < maxRetries; i++ { if err == nil { return pods, nil } - pods, err = c.Pods(namespace).List(label) + pods, err = c.Pods(namespace).List(label, field) } return pods, err } @@ -127,7 +127,7 @@ func RunRC(c *client.Client, name string, ns, image string, replicas int) { By(fmt.Sprintf("Making sure all %d replicas exist", replicas)) label := labels.SelectorFromSet(labels.Set(map[string]string{"name": name})) - pods, err := listPods(c, ns, label) + pods, err := listPods(c, ns, label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) current = len(pods.Items) failCount := 5 @@ -147,7 +147,7 @@ func RunRC(c *client.Client, name string, ns, image string, replicas int) { last = current time.Sleep(5 * time.Second) - pods, err = listPods(c, ns, label) + pods, err = listPods(c, ns, label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) current = len(pods.Items) } @@ -166,7 +166,7 @@ func RunRC(c *client.Client, name string, ns, image string, replicas int) { unknown := 0 time.Sleep(10 * time.Second) - currentPods, listErr := listPods(c, ns, label) + currentPods, listErr := listPods(c, ns, label, fields.Everything()) Expect(listErr).NotTo(HaveOccurred()) if len(currentPods.Items) != len(pods.Items) { Failf("Number of reported pods changed: %d vs %d", len(currentPods.Items), len(pods.Items)) diff --git a/test/e2e/es_cluster_logging.go b/test/e2e/es_cluster_logging.go index 2c93c2c33d0..11fff3d4785 100644 --- a/test/e2e/es_cluster_logging.go +++ b/test/e2e/es_cluster_logging.go @@ -83,7 +83,7 @@ func ClusterLevelLoggingWithElasticsearch(c *client.Client) { // Wait for the Elasticsearch pods to enter the running state. By("Checking to make sure the Elasticsearch pods are running") label := labels.SelectorFromSet(labels.Set(map[string]string{"name": "elasticsearch-logging"})) - pods, err := c.Pods(api.NamespaceDefault).List(label) + pods, err := c.Pods(api.NamespaceDefault).List(label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) for _, pod := range pods.Items { err = waitForPodRunning(c, pod.Name) diff --git a/test/e2e/events.go b/test/e2e/events.go index cc98d903618..ff7e6f6bcbc 100644 --- a/test/e2e/events.go +++ b/test/e2e/events.go @@ -78,7 +78,7 @@ var _ = Describe("Events", func() { expectNoError(waitForPodRunning(c, pod.Name)) By("verifying the pod is in kubernetes") - pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) Expect(len(pods.Items)).To(Equal(1)) By("retrieving the pod") diff --git a/test/e2e/monitoring.go b/test/e2e/monitoring.go index 5c5aec5243c..f426d0b4cfd 100644 --- a/test/e2e/monitoring.go +++ b/test/e2e/monitoring.go @@ -86,7 +86,7 @@ func verifyExpectedRcsExistAndGetExpectedPods(c *client.Client) ([]string, error return nil, fmt.Errorf("expected to find only one replica for rc %q, found %d", rc.Name, rc.Status.Replicas) } expectedRcs[rc.Name] = true - podList, err := c.Pods(api.NamespaceDefault).List(labels.Set(rc.Spec.Selector).AsSelector()) + podList, err := c.Pods(api.NamespaceDefault).List(labels.Set(rc.Spec.Selector).AsSelector(), fields.Everything()) if err != nil { return nil, err } diff --git a/test/e2e/networking.go b/test/e2e/networking.go index e16cf8d9c75..c6933b2c3f8 100644 --- a/test/e2e/networking.go +++ b/test/e2e/networking.go @@ -85,7 +85,7 @@ var _ = Describe("Networking", func() { //Assert basic external connectivity. //Since this is not really a test of kubernetes in any way, we //leave it as a pre-test assertion, rather than a Ginko test. - By("Executing a successfull http request from the external internet") + By("Executing a successful http request from the external internet") resp, err := http.Get("http://google.com") if err != nil { Failf("Unable to connect/talk to the internet: %v", err) diff --git a/test/e2e/pods.go b/test/e2e/pods.go index 6c041f3f8ba..1992ddc9a7e 100644 --- a/test/e2e/pods.go +++ b/test/e2e/pods.go @@ -174,7 +174,7 @@ var _ = Describe("Pods", func() { } By("setting up watch") - pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) if err != nil { Fail(fmt.Sprintf("Failed to query for pods: %v", err)) } @@ -196,7 +196,7 @@ var _ = Describe("Pods", func() { } By("verifying the pod is in kubernetes") - pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) if err != nil { Fail(fmt.Sprintf("Failed to query for pods: %v", err)) } @@ -214,7 +214,7 @@ var _ = Describe("Pods", func() { By("deleting the pod") podClient.Delete(pod.Name) - pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) if err != nil { Fail(fmt.Sprintf("Failed to delete pod: %v", err)) } @@ -286,7 +286,7 @@ var _ = Describe("Pods", func() { expectNoError(waitForPodRunning(c, pod.Name)) By("verifying the pod is in kubernetes") - pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) Expect(len(pods.Items)).To(Equal(1)) By("retrieving the pod") @@ -309,7 +309,7 @@ var _ = Describe("Pods", func() { expectNoError(waitForPodRunning(c, pod.Name)) By("verifying the updated pod is in kubernetes") - pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + pods, err = podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value})), fields.Everything()) Expect(len(pods.Items)).To(Equal(1)) fmt.Println("pod update OK") }) diff --git a/test/e2e/rc.go b/test/e2e/rc.go index 68fe8530a69..624342a26db 100644 --- a/test/e2e/rc.go +++ b/test/e2e/rc.go @@ -22,6 +22,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" @@ -110,7 +111,7 @@ func ServeImageOrFail(c *client.Client, test string, image string) { // List the pods, making sure we observe all the replicas. listTimeout := time.Minute label := labels.SelectorFromSet(labels.Set(map[string]string{"name": name})) - pods, err := c.Pods(ns).List(label) + pods, err := c.Pods(ns).List(label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) t := time.Now() for { @@ -123,7 +124,7 @@ func ServeImageOrFail(c *client.Client, test string, image string) { name, replicas, len(pods.Items), time.Since(t).Seconds()) } time.Sleep(5 * time.Second) - pods, err = c.Pods(ns).List(label) + pods, err = c.Pods(ns).List(label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) } @@ -165,7 +166,7 @@ type responseChecker struct { func (r responseChecker) checkAllResponses() (done bool, err error) { successes := 0 - currentPods, err := r.c.Pods(r.ns).List(r.label) + currentPods, err := r.c.Pods(r.ns).List(r.label, fields.Everything()) Expect(err).NotTo(HaveOccurred()) for i, pod := range r.pods.Items { // Check that the replica list remains unchanged, otherwise we have problems. diff --git a/test/e2e/service.go b/test/e2e/service.go index 8c3d5a11264..8c74c87027e 100644 --- a/test/e2e/service.go +++ b/test/e2e/service.go @@ -257,7 +257,7 @@ var _ = Describe("Services", func() { It("should be able to create a functioning external load balancer", func() { serviceName := "external-lb-test" - ns := api.NamespaceDefault + ns := namespace0 labels := map[string]string{ "key0": "value0", } @@ -275,9 +275,6 @@ var _ = Describe("Services", func() { }, } - By("cleaning up previous service " + serviceName + " from namespace " + ns) - c.Services(ns).Delete(serviceName) - By("creating service " + serviceName + " with external load balancer in namespace " + ns) result, err := c.Services(ns).Create(service) Expect(err).NotTo(HaveOccurred()) @@ -317,7 +314,7 @@ var _ = Describe("Services", func() { } By("creating pod to be part of service " + serviceName) - podClient := c.Pods(api.NamespaceDefault) + podClient := c.Pods(ns) defer func() { By("deleting pod " + pod.Name) defer GinkgoRecover() @@ -326,7 +323,7 @@ var _ = Describe("Services", func() { if _, err := podClient.Create(pod); err != nil { Failf("Failed to create pod %s: %v", pod.Name, err) } - expectNoError(waitForPodRunning(c, pod.Name)) + expectNoError(waitForPodRunningInNamespace(c, pod.Name, ns)) By("hitting the pod through the service's external load balancer") var resp *http.Response diff --git a/test/integration/client_test.go b/test/integration/client_test.go index ad43765d027..65f32870e7a 100644 --- a/test/integration/client_test.go +++ b/test/integration/client_test.go @@ -56,7 +56,7 @@ func TestClient(t *testing.T) { t.Errorf("expected %#v, got %#v", e, a) } - pods, err := client.Pods(ns).List(labels.Everything()) + pods, err := client.Pods(ns).List(labels.Everything(), fields.Everything()) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -94,7 +94,7 @@ func TestClient(t *testing.T) { } // pod is shown, but not scheduled - pods, err = client.Pods(ns).List(labels.Everything()) + pods, err = client.Pods(ns).List(labels.Everything(), fields.Everything()) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/test/integration/metrics_test.go b/test/integration/metrics_test.go index 0e331861a91..f859f03832e 100644 --- a/test/integration/metrics_test.go +++ b/test/integration/metrics_test.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/golang/glog" "github.com/golang/protobuf/proto" @@ -111,7 +112,7 @@ func TestApiserverMetrics(t *testing.T) { // Make a request to the apiserver to ensure there's at least one data point // for the metrics we're expecting -- otherwise, they won't be exported. client := client.NewOrDie(&client.Config{Host: s.URL, Version: testapi.Version()}) - if _, err := client.Pods(api.NamespaceDefault).List(labels.Everything()); err != nil { + if _, err := client.Pods(api.NamespaceDefault).List(labels.Everything(), fields.Everything()); err != nil { t.Fatalf("unexpected error getting pods: %v", err) } diff --git a/test/integration/secret_test.go b/test/integration/secret_test.go index a031df8d64f..a936f3074b1 100644 --- a/test/integration/secret_test.go +++ b/test/integration/secret_test.go @@ -39,7 +39,7 @@ func init() { func deletePodOrErrorf(t *testing.T, c *client.Client, ns, name string) { if err := c.Pods(ns).Delete(name); err != nil { - t.Errorf("unable to delete pods %v: %v", name, err) + t.Errorf("unable to delete pod %v: %v", name, err) } } func deleteSecretOrErrorf(t *testing.T, c *client.Client, ns, name string) { @@ -136,7 +136,7 @@ func DoTestSecrets(t *testing.T, client *client.Client, apiVersion string) { defer deletePodOrErrorf(t, client, ns, pod.Name) // Create a pod that consumes non-existent secret. - pod.ObjectMeta.Name = "uses-non-existant-secret" + pod.ObjectMeta.Name = "uses-non-existent-secret" if _, err := client.Pods(ns).Create(pod); err != nil { t.Errorf("Failed to create pod: %v", err) } diff --git a/www/README.md b/www/README.md index e5e8c844953..41ac8b6a0e0 100644 --- a/www/README.md +++ b/www/README.md @@ -30,7 +30,7 @@ Bower components should be refernced in one of the `vendor.json` files below: ### Serving the app during development -The app can be served through `kubectl`, but for some types of review a local web server is convenient. One can be installed as follows: +For development you can serve the files locally by installing a webserver as follows: ``` sudo npm install -g http-server @@ -43,6 +43,9 @@ cd app http-server -a localhost -p 8000 ``` +### Serving the app in production +https:///static/app/ + ### Configuration #### Configuration settings A json file can be used by `gulp` to automatically create angular constants. This is useful for setting per environment variables such as api endpoints. @@ -57,7 +60,6 @@ www/master ├── shared/config/development.json └── components ├── dashboard/config/development.json - ├── graph/config/development.json └── my_component/config/development.json ``` produces ```www/master/shared/config/generated-config.js```: @@ -66,14 +68,16 @@ angular.module('kubernetesApp.config', []) .constant('ENV', { '/': , 'dashboard': , - 'graph': , 'my_component': }); ``` #### Kubernetes server configuration -**RECOMMENDED**: By default the Kubernetes api server does not support CORS, +You'll need to run ```hack/build-ui.sh``` to create a new ```pkg/ui/datafile.go``` file. +This is the file that is built-in to the kube-apiserver. + +**RECOMMENDED**: When working in development mode the Kubernetes api server does not support CORS, so the `kube-apiserver.service` must be started with `--cors_allowed_origins=.*` or `--cors_allowed_origins=http://` @@ -87,7 +91,7 @@ angular.module('kubernetesApp.config', []) See [master/components/README.md](master/components/README.md). ### Testing -Currently kuberntes-ui includes both unit-testing (run via [Karma](http://karma-runner.github.io/0.12/index.html)) and +Currently kubernetes/www includes both unit-testing (run via [Karma](http://karma-runner.github.io/0.12/index.html)) and end-to-end testing (run via [Protractor](http://angular.github.io/protractor/#/)).