diff --git a/src/app/backend/deploy.go b/src/app/backend/deploy.go index 343b7aa7b8e40d8e914d71ce43a00efa634cb5a2..cce77cbdafbf3ff6f1eaf4e53e6b3e2d1295af41 100644 --- a/src/app/backend/deploy.go +++ b/src/app/backend/deploy.go @@ -17,7 +17,8 @@ package main import ( "log" - api "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/util/intstr" ) @@ -57,6 +58,12 @@ type AppDeploymentSpec struct { // Target namespace of the application. Namespace string `json:"namespace"` + // Optional memory requirement for the container. + MemoryRequirement *resource.Quantity `json:"memoryRequirement"` + + // Optional CPU requirement for the container. + CpuRequirement *resource.Quantity `json:"cpuRequirement"` + // Labels that will be defined on Pods/RCs/Services Labels []Label `json:"labels"` @@ -88,7 +95,6 @@ type Label struct { // Deploys an app based on the given configuration. The app is deployed using the given client. // App deployment consists of a replication controller and an optional service. Both of them share // common labels. -// TODO(bryk): Write tests for this function. func DeployApp(spec *AppDeploymentSpec, client client.Interface) error { log.Printf("Deploying %s application into %s namespace", spec.Name, spec.Namespace) @@ -109,16 +115,25 @@ func DeployApp(spec *AppDeploymentSpec, client client.Interface) error { SecurityContext: &api.SecurityContext{ Privileged: &spec.RunAsPrivileged, }, + Resources: api.ResourceRequirements{ + Requests: make(map[api.ResourceName]resource.Quantity), + }, } if spec.ContainerCommand != nil { containerSpec.Command = []string{*spec.ContainerCommand} } - if spec.ContainerCommandArgs != nil { containerSpec.Args = []string{*spec.ContainerCommandArgs} } + if spec.CpuRequirement != nil { + containerSpec.Resources.Requests[api.ResourceCPU] = *spec.CpuRequirement + } + if spec.MemoryRequirement != nil { + containerSpec.Resources.Requests[api.ResourceMemory] = *spec.MemoryRequirement + } + podTemplate := &api.PodTemplateSpec{ ObjectMeta: objectMeta, Spec: api.PodSpec{ diff --git a/src/app/externs/backendapi.js b/src/app/externs/backendapi.js index 646325de2cdc0738203908bde2ccd564b04aec24..453d20ccc37cd394a47b88cfff11191b67f59d3c 100644 --- a/src/app/externs/backendapi.js +++ b/src/app/externs/backendapi.js @@ -53,6 +53,8 @@ backendApi.Label; * labels: !Array, * replicas: number, * namespace: string, + * memoryRequirement: ?string, + * cpuRequirement: ?number, * runAsPrivileged: boolean, * }} */ diff --git a/src/app/frontend/deploy/deployfromsettings.html b/src/app/frontend/deploy/deployfromsettings.html index 2075dd615b8454cce6d3f5976c90f8a044233d58..91c8584de436ec5e78e20b39b75b6e17d0cebe63 100644 --- a/src/app/frontend/deploy/deployfromsettings.html +++ b/src/app/frontend/deploy/deployfromsettings.html @@ -17,7 +17,8 @@ limitations under the License. - + @@ -42,7 +43,8 @@ limitations under the License. - Enter the URL of a public image on any registry, or a private image hosted on Docker Hub or Google Container Registry. + Enter the URL of a public image on any registry, or a private image hosted on Docker Hub or + Google Container Registry. Learn more @@ -50,7 +52,11 @@ limitations under the License. - + + + Number of pods must be a positive integer. + Number of pods must be positive. + A Replica Set will be created to maintain the desired number of pods across your cluster. @@ -59,27 +65,40 @@ limitations under the License. -
- - - - - - - - - - - - - {{protocol}} - - - +
+ + + + + + Port must be an integer. + Port must be non-negative. + + + + + + + Target port must be an integer. + Target port must be non-negative. + + + + + + + {{protocol}} + + + +
- Ports are optional. If specified, a Service will be created mapping the Port (incoming) to a target Port seen by the container. - The internal DNS name for this Service will be: {{ctrl.name}}. + Ports are optional. If specified, a Service will be created mapping the Port (incoming) to a + target Port seen by the container. + + The internal DNS name for this Service will be: {{ctrl.name}}. + Learn more @@ -93,17 +112,18 @@ limitations under the License.
- + - The description will be added as an annotation to the Replica Set and displayed in the application's details. + The description will be added as an annotation to the Replica Set and displayed in the + application's details.
-
Labels (optional)
+
Labels

Key

@@ -115,7 +135,8 @@ limitations under the License.
- The specified labels will be applied to the created Replica Set, Service (if any) and Pods. Common labels include release, environment, tier, partition and track. + The specified labels will be applied to the created Replica Set, Service (if any) and Pods. + Common labels include release, environment, tier, partition and track. Learn more @@ -138,15 +159,40 @@ limitations under the License. + +
+ + + + + CPU requirement must be a valid number. + CPU requirement must be positive. + + + + + + + Memory requirement must be a valid number. + Memory requirement must be positive. + + +
+ + You can specify minimum CPU and memory requirements for the container. + Learn more + +
+
- + - +
diff --git a/src/app/frontend/deploy/deployfromsettings_controller.js b/src/app/frontend/deploy/deployfromsettings_controller.js index 48827e0982aa0260be09a0c97766e70b59284065..4c53554940fe04d7d56066c3661ab8ed0413fcbe 100644 --- a/src/app/frontend/deploy/deployfromsettings_controller.js +++ b/src/app/frontend/deploy/deployfromsettings_controller.js @@ -112,6 +112,16 @@ export default class DeployFromSettingsController { */ this.namespace = this.namespaces[0]; + /** + * @export {?number} + */ + this.cpuRequirement = null; + + /** + * @type {?number} + */ + this.memoryRequirement = null; + /** @private {!angular.$q} */ this.q_ = $q; @@ -147,6 +157,9 @@ export default class DeployFromSettingsController { portMappings: this.portMappings.filter(this.isPortMappingEmpty_), replicas: this.replicas, namespace: this.namespace, + cpuRequirement: angular.isNumber(this.cpuRequirement) ? this.cpuRequirement : null, + memoryRequirement: angular.isNumber(this.memoryRequirement) ? `${this.memoryRequirement}Mi` : + null, labels: this.toBackendApiLabels_(this.labels), runAsPrivileged: this.runAsPrivileged, }; diff --git a/src/test/backend/deploy_test.go b/src/test/backend/deploy_test.go index 9017a86bd4220babbb51516826468634691e705c..a7fe7b3d5cb16cc13f681cf01788d71a2d3b4430 100644 --- a/src/test/backend/deploy_test.go +++ b/src/test/backend/deploy_test.go @@ -19,6 +19,7 @@ import ( "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) @@ -48,6 +49,9 @@ func TestDeployApp(t *testing.T) { SecurityContext: &api.SecurityContext{ Privileged: &spec.RunAsPrivileged, }, + Resources: api.ResourceRequirements{ + Requests: make(map[api.ResourceName]resource.Quantity), + }, }}, }, }, @@ -102,3 +106,32 @@ func TestDeployAppContainerCommands(t *testing.T) { commandArgs, container.Args) } } + +func TestDeployWithResourceRequirements(t *testing.T) { + cpuRequirement := resource.Quantity{} + memoryRequirement := resource.Quantity{} + spec := &AppDeploymentSpec{ + Namespace: "foo-namespace", + Name: "foo-name", + CpuRequirement: &cpuRequirement, + MemoryRequirement: &memoryRequirement, + } + expectedResources := api.ResourceRequirements{ + Requests: map[api.ResourceName]resource.Quantity{ + api.ResourceMemory: memoryRequirement, + api.ResourceCPU: cpuRequirement, + }, + } + testClient := testclient.NewSimpleFake() + + DeployApp(spec, testClient) + + createAction := testClient.Actions()[0].(testclient.CreateActionImpl) + + rc := createAction.GetObject().(*api.ReplicationController) + container := rc.Spec.Template.Spec.Containers[0] + if !reflect.DeepEqual(container.Resources, expectedResources) { + t.Errorf("Expected resource requirements to be %#v but got %#v", + expectedResources, container.Resources) + } +} diff --git a/src/test/frontend/deploy/deployfromsettings_controller_test.js b/src/test/frontend/deploy/deployfromsettings_controller_test.js index bfe03b49ae09e550bdb6b2e1f20bc3f8731bf7f5..5e95291497b941e6be333e1c294d1f3398f21959 100644 --- a/src/test/frontend/deploy/deployfromsettings_controller_test.js +++ b/src/test/frontend/deploy/deployfromsettings_controller_test.js @@ -172,6 +172,48 @@ describe('DeployFromSettings controller', () => { expect(resourceObject.save).toHaveBeenCalled(); }); + it('should deploy with resource requirements', () => { + // given + let resourceObject = { + save: jasmine.createSpy('save'), + }; + mockResource.and.returnValue(resourceObject); + resourceObject.save.and.callFake(function(spec) { + // then + expect(spec.cpuRequirement).toBe(77); + expect(spec.memoryRequirement).toBe('88Mi'); + }); + ctrl.cpuRequirement = 77; + ctrl.memoryRequirement = 88; + + // when + ctrl.deploy(); + + // then + expect(resourceObject.save).toHaveBeenCalled(); + }); + + it('should deploy with empty resource requirements', () => { + // given + let resourceObject = { + save: jasmine.createSpy('save'), + }; + mockResource.and.returnValue(resourceObject); + resourceObject.save.and.callFake(function(spec) { + // then + expect(spec.cpuRequirement).toBe(null); + expect(spec.memoryRequirement).toBe(null); + }); + ctrl.cpuRequirement = null; + ctrl.memoryRequirement = ''; + + // when + ctrl.deploy(); + + // then + expect(resourceObject.save).toHaveBeenCalled(); + }); + it('should hide more options by default', () => { // this is default behavior so no given/when // then