diff --git a/assets/components/openshift-dns/dns/daemonset.yaml b/assets/components/openshift-dns/dns/daemonset.yaml index 5faae9a3e8..248245b363 100644 --- a/assets/components/openshift-dns/dns/daemonset.yaml +++ b/assets/components/openshift-dns/dns/daemonset.yaml @@ -57,8 +57,15 @@ spec: failureThreshold: 5 resources: requests: - cpu: 50m - memory: 70Mi + {{- range $key, $value := .DNSRequests }} + {{ $key }}: {{ $value }} + {{- end }} + {{- if .DNSLimits }} + limits: + {{- range $key, $value := .DNSLimits }} + {{ $key }}: {{ $value }} + {{- end }} + {{- end }} securityContext: readOnlyRootFilesystem: true image: '{{ .ReleaseImage.coredns }}' diff --git a/cmd/generate-config/config/config-openapi-spec.json b/cmd/generate-config/config/config-openapi-spec.json index 901548610d..7783407006 100755 --- a/cmd/generate-config/config/config-openapi-spec.json +++ b/cmd/generate-config/config/config-openapi-spec.json @@ -266,6 +266,26 @@ "example": "Enabled" } } + }, + "resources": { + "description": "Resources configures the CPU and memory resources for the dns container.", + "type": "object", + "properties": { + "limits": { + "description": "Limits specifies the maximum resources the dns container can use.\nValid keys are \"cpu\" and \"memory\". Values must be valid Kubernetes resource quantities.\nWhen not set, no limits are applied.", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "requests": { + "description": "Requests specifies the minimum resources required for the dns container.\nValid keys are \"cpu\" and \"memory\". Values must be valid Kubernetes resource quantities.\nWhen not set, defaults to cpu=50m, memory=70Mi.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } } } }, diff --git a/docs/user/howto_config.md b/docs/user/howto_config.md index cbed9baa28..16f7183bc9 100644 --- a/docs/user/howto_config.md +++ b/docs/user/howto_config.md @@ -43,6 +43,9 @@ dns: hosts: file: "" status: "" + resources: + limits: {} + requests: {} etcd: memoryLimitMB: 0 genericDevicePlugin: @@ -201,6 +204,9 @@ dns: hosts: file: /etc/hosts status: Disabled + resources: + limits: {} + requests: {} etcd: memoryLimitMB: 0 genericDevicePlugin: diff --git a/packaging/microshift/config.yaml b/packaging/microshift/config.yaml index fd45f1a37a..2239a1e9b4 100644 --- a/packaging/microshift/config.yaml +++ b/packaging/microshift/config.yaml @@ -85,6 +85,16 @@ dns: # example: # Enabled status: Disabled + # Resources configures the CPU and memory resources for the dns container. + resources: + # Limits specifies the maximum resources the dns container can use. + # Valid keys are "cpu" and "memory". Values must be valid Kubernetes resource quantities. + # When not set, no limits are applied. + limits: {} + # Requests specifies the minimum resources required for the dns container. + # Valid keys are "cpu" and "memory". Values must be valid Kubernetes resource quantities. + # When not set, defaults to cpu=50m, memory=70Mi. + requests: {} etcd: # Set a memory limit on the etcd process; etcd will begin paging # memory when it gets to this value. 0 means no limit. diff --git a/pkg/components/controllers.go b/pkg/components/controllers.go index e9bdf7af50..9c4e50747d 100644 --- a/pkg/components/controllers.go +++ b/pkg/components/controllers.go @@ -306,6 +306,8 @@ func startDNSController(ctx context.Context, cfg *config.Config, kubeconfigPath extraParams := assets.RenderParams{ "ClusterIP": cfg.Network.DNS, "HostsEnabled": cfg.DNS.Hosts.Status == config.HostsStatusEnabled, + "DNSRequests": cfg.DNS.Resources.Requests, + "DNSLimits": cfg.DNS.Resources.Limits, } if err := assets.ApplyServices(ctx, svc, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index cffb723f2c..54498be5cb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -439,6 +439,24 @@ func (c *Config) incorporateUserSettings(u *Config) { c.DNS.Hosts.File = u.DNS.Hosts.File } } + + // DNS resource configuration - merge key-by-key to preserve defaults + if u.DNS.Resources.Requests != nil { + if c.DNS.Resources.Requests == nil { + c.DNS.Resources.Requests = make(map[string]string) + } + for k, v := range u.DNS.Resources.Requests { + c.DNS.Resources.Requests[k] = v + } + } + if u.DNS.Resources.Limits != nil { + if c.DNS.Resources.Limits == nil { + c.DNS.Resources.Limits = make(map[string]string) + } + for k, v := range u.DNS.Resources.Limits { + c.DNS.Resources.Limits[k] = v + } + } if u.ApiServer.FeatureGates.FeatureSet != "" { c.ApiServer.FeatureGates.FeatureSet = u.ApiServer.FeatureGates.FeatureSet } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 523a53c69b..5f3a15a8c6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -79,6 +79,63 @@ func TestGetActiveConfigFromYAML(t *testing.T) { return c }(), }, + { + name: "dns-resources-requests", + config: dedent(` + dns: + resources: + requests: + cpu: "100m" + memory: "150Mi" + `), + expected: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests = map[string]string{ + "cpu": "100m", + "memory": "150Mi", + } + return c + }(), + }, + { + name: "dns-resources-partial-request", + config: dedent(` + dns: + resources: + requests: + cpu: "100m" + `), + expected: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["cpu"] = "100m" + return c + }(), + }, + { + name: "dns-resources-with-limits", + config: dedent(` + dns: + resources: + requests: + cpu: "100m" + memory: "150Mi" + limits: + cpu: "200m" + memory: "256Mi" + `), + expected: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests = map[string]string{ + "cpu": "100m", + "memory": "150Mi", + } + c.DNS.Resources.Limits = map[string]string{ + "cpu": "200m", + "memory": "256Mi", + } + return c + }(), + }, { name: "network", config: dedent(` @@ -904,6 +961,113 @@ func TestValidate(t *testing.T) { }(), expectErr: true, }, + { + name: "dns-resources-valid-quantities", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests = map[string]string{ + "cpu": "100m", + "memory": "128Mi", + } + c.DNS.Resources.Limits = map[string]string{ + "cpu": "200m", + "memory": "256Mi", + } + return c + }(), + expectErr: false, + }, + { + name: "dns-resources-invalid-request", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["cpu"] = "abc" + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-invalid-limit", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Limits = map[string]string{ + "cpu": "not-a-quantity", + } + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-limit-less-than-request", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["cpu"] = "200m" + c.DNS.Resources.Limits = map[string]string{ + "cpu": "50m", + } + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-limit-without-request", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Limits = map[string]string{ + "cpu": "200m", + } + return c + }(), + expectErr: false, + }, + { + name: "dns-resources-unsupported-request-key", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["gpu"] = "1" + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-unsupported-limit-key", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Limits = map[string]string{ + "ephemeral-storage": "1Gi", + } + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-cpu-below-minimum", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["cpu"] = "10m" + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-memory-below-minimum", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["memory"] = "30Mi" + return c + }(), + expectErr: true, + }, + { + name: "dns-resources-at-minimum", + config: func() *Config { + c := mkDefaultConfig() + c.DNS.Resources.Requests["cpu"] = "50m" + c.DNS.Resources.Requests["memory"] = "70Mi" + return c + }(), + expectErr: false, + }, } for _, tt := range ttests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/config/dns.go b/pkg/config/dns.go index d8948449c9..c58dcffd7c 100644 --- a/pkg/config/dns.go +++ b/pkg/config/dns.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + + "k8s.io/apimachinery/pkg/api/resource" ) const ( @@ -13,6 +15,20 @@ const ( type HostsStatusEnum string +// DNSResources configures the CPU and memory resources for the dns container +// in the dns-default DaemonSet. +type DNSResources struct { + // Requests specifies the minimum resources required for the dns container. + // Valid keys are "cpu" and "memory". Values must be valid Kubernetes resource quantities. + // When not set, defaults to cpu=50m, memory=70Mi. + Requests map[string]string `json:"requests,omitempty"` + + // Limits specifies the maximum resources the dns container can use. + // Valid keys are "cpu" and "memory". Values must be valid Kubernetes resource quantities. + // When not set, no limits are applied. + Limits map[string]string `json:"limits,omitempty"` +} + type DNS struct { // baseDomain is the base domain of the cluster. All managed DNS records will // be sub-domains of this base. @@ -29,6 +45,9 @@ type DNS struct { // Hosts contains configuration for the hosts file. Hosts HostsConfig `json:"hosts,omitempty"` + + // Resources configures the CPU and memory resources for the dns container. + Resources DNSResources `json:"resources,omitempty"` } // HostsConfig contains configuration for the hosts file . @@ -55,14 +74,27 @@ func dnsDefaults() DNS { File: "/etc/hosts", Status: HostsStatusDisabled, }, + Resources: DNSResources{ + Requests: map[string]string{ + "cpu": "50m", + "memory": "70Mi", + }, + }, } } func (t *DNS) validate() error { + if err := t.validateHosts(); err != nil { + return err + } + return t.validateResources() +} + +func (t *DNS) validateHosts() error { switch t.Hosts.Status { case HostsStatusEnabled: if t.Hosts.File == "" { - break + return nil } cleanPath := filepath.Clean(t.Hosts.File) @@ -94,5 +126,53 @@ func (t *DNS) validate() error { default: return fmt.Errorf("invalid hosts status: %s", t.Hosts.Status) } +} + +func dnsMinimumRequests() map[string]resource.Quantity { + defaults := dnsDefaults() + mins := make(map[string]resource.Quantity, len(defaults.Resources.Requests)) + for k, v := range defaults.Resources.Requests { + mins[k] = resource.MustParse(v) + } + return mins +} + +func (t *DNS) validateResources() error { + allowed := map[string]struct{}{ + "cpu": {}, + "memory": {}, + } + mins := dnsMinimumRequests() + for key, val := range t.Resources.Requests { + if _, ok := allowed[key]; !ok { + return fmt.Errorf("unsupported dns resource request key %q: allowed keys are cpu, memory", key) + } + qty, err := resource.ParseQuantity(val) + if err != nil { + return fmt.Errorf("invalid dns resource request %s=%q: %v", key, val, err) + } + if minQty, ok := mins[key]; ok && qty.Cmp(minQty) < 0 { + return fmt.Errorf("dns resource request %s=%q is below minimum %s", key, val, minQty.String()) + } + } + for key, val := range t.Resources.Limits { + if _, ok := allowed[key]; !ok { + return fmt.Errorf("unsupported dns resource limit key %q: allowed keys are cpu, memory", key) + } + if _, err := resource.ParseQuantity(val); err != nil { + return fmt.Errorf("invalid dns resource limit %s=%q: %v", key, val, err) + } + } + for key, limitVal := range t.Resources.Limits { + reqVal, ok := t.Resources.Requests[key] + if !ok { + continue + } + limit := resource.MustParse(limitVal) + req := resource.MustParse(reqVal) + if limit.Cmp(req) < 0 { + return fmt.Errorf("dns resource limit %s=%q must be greater than or equal to request %s=%q", key, limitVal, key, reqVal) + } + } return nil } diff --git a/test/suites/configuration/dns-resource-configuration.robot b/test/suites/configuration/dns-resource-configuration.robot new file mode 100644 index 0000000000..e4cd48f0f1 --- /dev/null +++ b/test/suites/configuration/dns-resource-configuration.robot @@ -0,0 +1,184 @@ +*** Settings *** +Documentation DNS resource configuration tests + +Resource ../../resources/common.resource +Resource ../../resources/oc.resource +Resource ../../resources/microshift-config.resource +Resource ../../resources/microshift-process.resource +Library ../../resources/journalctl.py + +Suite Setup Setup +Suite Teardown Teardown + +Test Tags slow restart + + +*** Variables *** +${CURSOR} ${EMPTY} +${DNS_DROPIN} 10-dns-resources +${DNS_RESOURCE_PATH} .spec.template.spec.containers[0].resources +${DNS_CUSTOM_RESOURCES} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ requests: +... \ \ \ \ cpu: "100m" +... \ \ \ \ memory: "150Mi" +... \ \ \ limits: +... \ \ \ \ cpu: "200m" +... \ \ \ \ memory: "256Mi" +${DNS_REQUESTS_ONLY} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ requests: +... \ \ \ \ cpu: "100m" +... \ \ \ \ memory: "150Mi" +${DNS_PARTIAL_REQUESTS} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ requests: +... \ \ \ \ cpu: "100m" +${DNS_INVALID_QUANTITY} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ requests: +... \ \ \ \ cpu: "abc" +${DNS_LIMITS_ONLY} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ limits: +... \ \ \ \ cpu: "200m" +... \ \ \ \ memory: "256Mi" +${DNS_LIMIT_LESS_THAN_REQUEST} SEPARATOR=\n +... --- +... dns: +... \ \ resources: +... \ \ \ requests: +... \ \ \ \ cpu: "200m" +... \ \ \ limits: +... \ \ \ \ cpu: "50m" + + +*** Test Cases *** +Default DNS Resources + [Documentation] Verify default DNS resources when no custom config is applied + [Setup] Remove DNS Resource Config + ${cpu}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.cpu + ${memory}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.memory + Should Be Equal As Strings ${cpu} 50m + Should Be Equal As Strings ${memory} 70Mi + +Custom DNS Resources With Requests And Limits + [Documentation] Configure custom CPU and memory requests and limits via drop-in config + [Setup] Apply DNS Resource Config ${DNS_CUSTOM_RESOURCES} + ${cpu_req}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.cpu + ${mem_req}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.memory + ${cpu_lim}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.limits.cpu + ${mem_lim}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.limits.memory + Should Be Equal As Strings ${cpu_req} 100m + Should Be Equal As Strings ${mem_req} 150Mi + Should Be Equal As Strings ${cpu_lim} 200m + Should Be Equal As Strings ${mem_lim} 256Mi + [Teardown] Remove DNS Resource Config + +Requests Only Without Limits + [Documentation] Configure only requests without limits and verify no limits are injected + [Setup] Apply DNS Resource Config ${DNS_REQUESTS_ONLY} + ${cpu}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.cpu + ${memory}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.memory + ${limits}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.limits + Should Be Equal As Strings ${cpu} 100m + Should Be Equal As Strings ${memory} 150Mi + Should Be Empty ${limits} + [Teardown] Remove DNS Resource Config + +Partial Requests Preserves Defaults + [Documentation] Configure only CPU request and verify memory default is preserved + [Setup] Apply DNS Resource Config ${DNS_PARTIAL_REQUESTS} + ${cpu}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.cpu + ${memory}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.memory + Should Be Equal As Strings ${cpu} 100m + Should Be Equal As Strings ${memory} 70Mi + [Teardown] Remove DNS Resource Config + +Limits Only Preserves Default Requests + [Documentation] Configure only limits and verify default requests are preserved + [Setup] Apply DNS Resource Config ${DNS_LIMITS_ONLY} + ${cpu_req}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.cpu + ${mem_req}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.requests.memory + ${cpu_lim}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.limits.cpu + ${mem_lim}= Get DNS Resource Value ${DNS_RESOURCE_PATH}.limits.memory + Should Be Equal As Strings ${cpu_req} 50m + Should Be Equal As Strings ${mem_req} 70Mi + Should Be Equal As Strings ${cpu_lim} 200m + Should Be Equal As Strings ${mem_lim} 256Mi + [Teardown] Remove DNS Resource Config + +Invalid Resource Quantity Prevents Start + [Documentation] Verify MicroShift fails to start with invalid resource quantity + [Setup] Apply Invalid DNS Resource Config ${DNS_INVALID_QUANTITY} + Pattern Should Appear In Log Output ${CURSOR} invalid dns resource request + [Teardown] Restore Valid DNS Config + +Limit Less Than Request Prevents Start + [Documentation] Verify MicroShift fails to start when limit is less than request + [Setup] Apply Invalid DNS Resource Config ${DNS_LIMIT_LESS_THAN_REQUEST} + Pattern Should Appear In Log Output ${CURSOR} must be greater than or equal to request + [Teardown] Restore Valid DNS Config + +DNS Resolution After Resource Change + [Documentation] Verify CoreDNS resolves cluster-local services after resource change + [Setup] Apply DNS Resource Config ${DNS_CUSTOM_RESOURCES} + ${output}= Oc Exec router-default nslookup kubernetes.default.svc.cluster.local + ... openshift-ingress deployment + Should Contain ${output} kubernetes.default.svc.cluster.local + [Teardown] Remove DNS Resource Config + + +*** Keywords *** +Setup + [Documentation] Test suite setup + Check Required Env Variables + Login MicroShift Host + Setup Kubeconfig + +Teardown + [Documentation] Test suite teardown + Remove Kubeconfig + Logout MicroShift Host + +Get DNS Resource Value + [Documentation] Get a resource value from the dns-default DaemonSet + [Arguments] ${jsonpath} + ${value}= Oc Get JsonPath daemonset openshift-dns dns-default ${jsonpath} + RETURN ${value} + +Apply DNS Resource Config + [Documentation] Remove any existing drop-in, apply a new DNS resource config and restart MicroShift + [Arguments] ${config} + Remove Drop In MicroShift Config ${DNS_DROPIN} + Drop In MicroShift Config ${config} ${DNS_DROPIN} + Restart MicroShift + +Apply Invalid DNS Resource Config + [Documentation] Apply an invalid DNS resource config that should prevent MicroShift from starting + [Arguments] ${config} + Remove Drop In MicroShift Config ${DNS_DROPIN} + Drop In MicroShift Config ${config} ${DNS_DROPIN} + ${cursor}= Get Journal Cursor + VAR ${CURSOR}= ${cursor} scope=TEST + Run Keyword And Expect Error 0 != 1 Restart MicroShift + +Remove DNS Resource Config + [Documentation] Remove the DNS resource drop-in config and restart MicroShift + Remove Drop In MicroShift Config ${DNS_DROPIN} + Restart MicroShift + +Restore Valid DNS Config + [Documentation] Remove invalid config and restore MicroShift to a working state + Remove Drop In MicroShift Config ${DNS_DROPIN} + Restart MicroShift