diff --git a/cmd/helm/init.go b/cmd/helm/init.go index b1a02bbb2..21daecdaa 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -26,6 +26,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/kubernetes" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/helm/cmd/helm/installer" "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/helm/helmpath" @@ -79,6 +80,10 @@ type initCmd struct { opts installer.Options kubeClient kubernetes.Interface serviceAccount string + cpuLimits string + cpuRequests string + memLimits string + memRequests string } func newInitCmd(out io.Writer) *cobra.Command { @@ -120,6 +125,10 @@ func newInitCmd(out io.Writer) *cobra.Command { f.BoolVar(&i.opts.EnableHostNetwork, "net-host", false, "install Tiller with net=host") f.StringVar(&i.serviceAccount, "service-account", "", "name of service account") + f.StringVar(&i.cpuLimits, "tiller-cpu-limits", "", "override tiller cpu limits") + f.StringVar(&i.cpuRequests, "tiller-cpu-requests", "", "override tiller cpu requests") + f.StringVar(&i.memLimits, "tiller-memory-limits", "", "override tiller cpu limits") + f.StringVar(&i.memRequests, "tiller-memory-requests", "", "override tiller cpu limits") return cmd } @@ -149,16 +158,50 @@ func (i *initCmd) tlsOptions() error { return nil } -// run initializes local config and installs Tiller to Kubernetes cluster. +func (i *initCmd) generateLimits() error { + var err error + if i.cpuLimits != "" { + i.opts.CPULimits, err = resource.ParseQuantity(i.cpuLimits) + if err != nil { + return err + } + } + + if i.cpuRequests != "" { + i.opts.CPURequests, err = resource.ParseQuantity(i.cpuRequests) + if err != nil { + return err + } + } + + if i.memLimits != "" { + i.opts.MemLimits, err = resource.ParseQuantity(i.memLimits) + if err != nil { + return err + } + } + + if i.memRequests != "" { + i.opts.MemRequests, err = resource.ParseQuantity(i.memRequests) + if err != nil { + return err + } + } + return nil +} + +// run initializes local config and installs tiller to Kubernetes Cluster. func (i *initCmd) run() error { if err := i.tlsOptions(); err != nil { return err } + if err := i.generateLimits(); err != nil { + return err + } i.opts.Namespace = i.namespace i.opts.UseCanary = i.canary i.opts.ImageSpec = i.image i.opts.ServiceAccount = i.serviceAccount - if settings.Debug { writeYAMLManifest := func(apiVersion, kind, body string, first, last bool) error { w := i.out diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index 55b62d284..09b2d0df3 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -302,3 +302,33 @@ func TestInitCmd_tlsOptions(t *testing.T) { } } } + +func TestInitCmd_resourceLimits(t *testing.T) { + + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + var buf bytes.Buffer + fc := fake.NewSimpleClientset() + + cmd := &initCmd{ + out: &buf, + home: helmpath.Home(home), + kubeClient: fc, + clientOnly: true, + dryRun: true, + namespace: v1.NamespaceDefault, + cpuLimits: "2", + cpuRequests: "100m", + memLimits: "1Gi", + memRequests: "500Mi", + } + if err := cmd.run(); err != nil { + t.Fatal(err) + } + if len(fc.Actions()) != 0 { + t.Error("expected client call") + } +} diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 7e8707fc8..6fbd6273a 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -21,6 +21,7 @@ import ( "github.com/ghodss/yaml" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" @@ -116,8 +117,43 @@ func generateLabels(labels map[string]string) map[string]string { return labels } +func generateLimits(opts *Options) (v1.ResourceRequirements, bool) { + limits := map[v1.ResourceName]resource.Quantity{} + requests := map[v1.ResourceName]resource.Quantity{} + resources := v1.ResourceRequirements{} + var resourcesEnabled bool + if (opts.CPURequests != (resource.Quantity{})) || (opts.CPULimits != (resource.Quantity{})) || + (opts.MemRequests != (resource.Quantity{})) || opts.MemLimits != (resource.Quantity{}) { + resourcesEnabled = true + if opts.CPULimits != (resource.Quantity{}) { + limits[v1.ResourceCPU] = opts.CPULimits + if opts.CPURequests == (resource.Quantity{}) { + requests[v1.ResourceCPU] = opts.CPULimits + } + } + if opts.CPURequests != (resource.Quantity{}) { + requests[v1.ResourceCPU] = opts.CPURequests + } + if opts.MemLimits != (resource.Quantity{}) { + limits[v1.ResourceMemory] = opts.MemLimits + if opts.MemRequests == (resource.Quantity{}) { + requests[v1.ResourceMemory] = opts.MemLimits + } + } + if opts.MemRequests != (resource.Quantity{}) { + requests[v1.ResourceMemory] = opts.MemRequests + } + resources = v1.ResourceRequirements{ + Limits: limits, + Requests: requests, + } + } + return resources, resourcesEnabled +} + func generateDeployment(opts *Options) *v1beta1.Deployment { labels := generateLabels(map[string]string{"name": "tiller"}) + resources, resourcesEnabled := generateLimits(opts) d := &v1beta1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Namespace: opts.Namespace, @@ -172,7 +208,9 @@ func generateDeployment(opts *Options) *v1beta1.Deployment { }, }, } - + if resourcesEnabled { + d.Spec.Template.Spec.Containers[0].Resources = resources + } if opts.tls() { const certsDir = "/etc/certs" diff --git a/cmd/helm/installer/install_test.go b/cmd/helm/installer/install_test.go index e1e94d7e5..42dba0842 100644 --- a/cmd/helm/installer/install_test.go +++ b/cmd/helm/installer/install_test.go @@ -30,6 +30,8 @@ import ( "k8s.io/client-go/pkg/apis/extensions/v1beta1" testcore "k8s.io/client-go/testing" + "fmt" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/helm/pkg/version" ) @@ -144,6 +146,91 @@ func TestDeploymentManifest_WithTLS(t *testing.T) { } } +func TestDeploymentManifest_WithResourceLimits(t *testing.T) { + + type test struct { + cpuLimits string + cpuRequests string + memLimits string + memRequests string + } + + var tests = []test{ + { + cpuLimits: "2", + cpuRequests: "100m", + memLimits: "1Gi", + memRequests: "500Mi", + }, + { + cpuLimits: "2", + memLimits: "1Gi", + }, + { + cpuRequests: "100m", + memRequests: "500Mi", + }, + {}, + } + + for _, tt := range tests { + opts := &Options{Namespace: v1.NamespaceDefault} + var err error + if tt.cpuLimits != "" { + opts.CPULimits, err = resource.ParseQuantity(tt.cpuLimits) + if err != nil { + t.Errorf("Error %q", err) + } + } + if tt.cpuRequests != "" { + opts.CPURequests, err = resource.ParseQuantity(tt.cpuRequests) + if err != nil { + t.Errorf("Error %q", err) + } + } + if tt.memLimits != "" { + opts.MemLimits, err = resource.ParseQuantity(tt.memLimits) + if err != nil { + t.Errorf("Error %q", err) + } + } + if tt.memRequests != "" { + opts.MemRequests, err = resource.ParseQuantity(tt.memRequests) + if err != nil { + t.Errorf("Error %q", err) + } + } + + o, err := DeploymentManifest(opts) + if err != nil { + t.Fatalf("error %q", err) + } + + var d v1beta1.Deployment + if err := yaml.Unmarshal([]byte(o), &d); err != nil { + t.Fatalf(" error %q", err) + } + fmt.Println(o) + // verify Resources in deployment reflect the use of cpu/memory limits. + if got := d.Spec.Template.Spec.Containers[0].Resources.Limits[v1.ResourceCPU]; got != opts.CPULimits { + t.Errorf("Expected cpu limits %q, got %q", opts.CPULimits, got) + } + + if got := d.Spec.Template.Spec.Containers[0].Resources.Limits[v1.ResourceMemory]; got != opts.MemLimits { + t.Errorf("Expected memory limits %q, got %q", opts.MemLimits, got) + } + + if got := d.Spec.Template.Spec.Containers[0].Resources.Requests[v1.ResourceCPU]; got != opts.CPURequests && got != opts.CPULimits { + t.Errorf("Expected cpu requests %q, got %q", opts.CPURequests, got) + } + + if got := d.Spec.Template.Spec.Containers[0].Resources.Requests[v1.ResourceMemory]; got != opts.MemRequests && got != opts.MemLimits { + t.Errorf("Expected memory requests %q, got %q", opts.MemRequests, got) + } + + } +} + func TestServiceManifest(t *testing.T) { o, err := ServiceManifest(v1.NamespaceDefault) if err != nil { diff --git a/cmd/helm/installer/options.go b/cmd/helm/installer/options.go index ddb7706f8..6997dca2f 100644 --- a/cmd/helm/installer/options.go +++ b/cmd/helm/installer/options.go @@ -19,6 +19,7 @@ package installer // import "k8s.io/helm/cmd/helm/installer" import ( "fmt" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/client-go/pkg/api/v1" "k8s.io/helm/pkg/version" ) @@ -71,6 +72,18 @@ type Options struct { // EnableHostNetwork installs Tiller with net=host. EnableHostNetwork bool + + //CPULimits is the cpu limits used to deploy tiller + CPULimits resource.Quantity + + //CPURequests is the cpu request limits used to deploy tiller + CPURequests resource.Quantity + + //MemLimits is the memory limits used to deploy tiller + MemLimits resource.Quantity + + //MemRequests is the memory request limits used to deploy tiller + MemRequests resource.Quantity } func (opts *Options) selectImage() string {