diff --git a/pkg/apis/config/v1alpha3/types.go b/pkg/apis/config/v1alpha3/types.go index 1eea364482..e117ce0522 100644 --- a/pkg/apis/config/v1alpha3/types.go +++ b/pkg/apis/config/v1alpha3/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha3 import ( + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/kind/pkg/container/cri" @@ -75,6 +76,9 @@ type Node struct { // ExtraPortMappings describes additional port mappings for the node container // binded to a host Port ExtraPortMappings []cri.PortMapping `json:"extraPortMappings,omitempty"` + + // Constraints describes the node resources constraints + Constraints NodeResources `json:"constraints,omitempty"` } // NodeRole defines possible role for nodes in a Kubernetes cluster managed by `kind` @@ -138,3 +142,11 @@ type PatchJSON6902 struct { // Patch should contain the contents of the json patch as a string Patch string `json:"patch"` } + +// NodeResources represents the node resources (CPU/Memory) +type NodeResources struct { + // The maximum amount of memory the node can use. + Memory resource.Quantity `json:"memory,omitempty"` + // Specify how much of the available CPU resources a node can use + Cpus resource.Quantity `json:"cpus,omitempty"` +} diff --git a/pkg/apis/config/v1alpha3/zz_generated.deepcopy.go b/pkg/apis/config/v1alpha3/zz_generated.deepcopy.go index 194aa31021..463864f683 100644 --- a/pkg/apis/config/v1alpha3/zz_generated.deepcopy.go +++ b/pkg/apis/config/v1alpha3/zz_generated.deepcopy.go @@ -97,6 +97,7 @@ func (in *Node) DeepCopyInto(out *Node) { *out = make([]cri.PortMapping, len(*in)) copy(*out, *in) } + in.Constraints.DeepCopyInto(&out.Constraints) return } @@ -110,6 +111,24 @@ func (in *Node) DeepCopy() *Node { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeResources) DeepCopyInto(out *NodeResources) { + *out = *in + out.Memory = in.Memory.DeepCopy() + out.Cpus = in.Cpus.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeResources. +func (in *NodeResources) DeepCopy() *NodeResources { + if in == nil { + return nil + } + out := new(NodeResources) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchJSON6902) DeepCopyInto(out *PatchJSON6902) { *out = *in diff --git a/pkg/internal/apis/config/types.go b/pkg/internal/apis/config/types.go index 77bf287318..7618382390 100644 --- a/pkg/internal/apis/config/types.go +++ b/pkg/internal/apis/config/types.go @@ -17,6 +17,7 @@ limitations under the License. package config import ( + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/kind/pkg/container/cri" @@ -74,6 +75,9 @@ type Node struct { // ExtraPortMappings describes additional port mappings for the node container // binded to a host Port ExtraPortMappings []cri.PortMapping + + // Constraints describes the node resources constraints + Constraints NodeResources } // NodeRole defines possible role for nodes in a Kubernetes cluster managed by `kind` @@ -137,3 +141,11 @@ type PatchJSON6902 struct { // Patch should contain the contents of the json patch as a string Patch string } + +// NodeResources represents the node resources (CPU/Memory) +type NodeResources struct { + // The maximum amount of memory the node can use. + Memory resource.Quantity + // Specify how much of the available CPU resources a node can use. + Cpus resource.Quantity +} diff --git a/pkg/internal/apis/config/v1alpha3/zz_generated.conversion.go b/pkg/internal/apis/config/v1alpha3/zz_generated.conversion.go index 4fe4037e7d..6c28721c8e 100644 --- a/pkg/internal/apis/config/v1alpha3/zz_generated.conversion.go +++ b/pkg/internal/apis/config/v1alpha3/zz_generated.conversion.go @@ -67,6 +67,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*v1alpha3.NodeResources)(nil), (*config.NodeResources)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_NodeResources_To_config_NodeResources(a.(*v1alpha3.NodeResources), b.(*config.NodeResources), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*config.NodeResources)(nil), (*v1alpha3.NodeResources)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_config_NodeResources_To_v1alpha3_NodeResources(a.(*config.NodeResources), b.(*v1alpha3.NodeResources), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*v1alpha3.PatchJSON6902)(nil), (*config.PatchJSON6902)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha3_PatchJSON6902_To_config_PatchJSON6902(a.(*v1alpha3.PatchJSON6902), b.(*config.PatchJSON6902), scope) }); err != nil { @@ -145,6 +155,9 @@ func autoConvert_v1alpha3_Node_To_config_Node(in *v1alpha3.Node, out *config.Nod out.Image = in.Image out.ExtraMounts = *(*[]cri.Mount)(unsafe.Pointer(&in.ExtraMounts)) out.ExtraPortMappings = *(*[]cri.PortMapping)(unsafe.Pointer(&in.ExtraPortMappings)) + if err := Convert_v1alpha3_NodeResources_To_config_NodeResources(&in.Constraints, &out.Constraints, s); err != nil { + return err + } return nil } @@ -158,6 +171,9 @@ func autoConvert_config_Node_To_v1alpha3_Node(in *config.Node, out *v1alpha3.Nod out.Image = in.Image out.ExtraMounts = *(*[]cri.Mount)(unsafe.Pointer(&in.ExtraMounts)) out.ExtraPortMappings = *(*[]cri.PortMapping)(unsafe.Pointer(&in.ExtraPortMappings)) + if err := Convert_config_NodeResources_To_v1alpha3_NodeResources(&in.Constraints, &out.Constraints, s); err != nil { + return err + } return nil } @@ -166,6 +182,28 @@ func Convert_config_Node_To_v1alpha3_Node(in *config.Node, out *v1alpha3.Node, s return autoConvert_config_Node_To_v1alpha3_Node(in, out, s) } +func autoConvert_v1alpha3_NodeResources_To_config_NodeResources(in *v1alpha3.NodeResources, out *config.NodeResources, s conversion.Scope) error { + out.Memory = in.Memory + out.Cpus = in.Cpus + return nil +} + +// Convert_v1alpha3_NodeResources_To_config_NodeResources is an autogenerated conversion function. +func Convert_v1alpha3_NodeResources_To_config_NodeResources(in *v1alpha3.NodeResources, out *config.NodeResources, s conversion.Scope) error { + return autoConvert_v1alpha3_NodeResources_To_config_NodeResources(in, out, s) +} + +func autoConvert_config_NodeResources_To_v1alpha3_NodeResources(in *config.NodeResources, out *v1alpha3.NodeResources, s conversion.Scope) error { + out.Memory = in.Memory + out.Cpus = in.Cpus + return nil +} + +// Convert_config_NodeResources_To_v1alpha3_NodeResources is an autogenerated conversion function. +func Convert_config_NodeResources_To_v1alpha3_NodeResources(in *config.NodeResources, out *v1alpha3.NodeResources, s conversion.Scope) error { + return autoConvert_config_NodeResources_To_v1alpha3_NodeResources(in, out, s) +} + func autoConvert_v1alpha3_PatchJSON6902_To_config_PatchJSON6902(in *v1alpha3.PatchJSON6902, out *config.PatchJSON6902, s conversion.Scope) error { out.Group = in.Group out.Version = in.Version diff --git a/pkg/internal/apis/config/validate.go b/pkg/internal/apis/config/validate.go index ab96026ba3..ed465a0edf 100644 --- a/pkg/internal/apis/config/validate.go +++ b/pkg/internal/apis/config/validate.go @@ -19,6 +19,8 @@ package config import ( "net" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/kind/pkg/errors" ) @@ -101,6 +103,17 @@ func (n *Node) Validate() error { } } + // validate node resource constraints + if n.Constraints.Cpus.Sign() < 0 { + errs = append(errs, errors.New("invalid number of Cpus")) + } + + // minimum memory size is 4m + minMemory := resource.MustParse("4m") + if n.Constraints.Memory.Sign() != 0 && n.Constraints.Memory.Cmp(minMemory) < 0 { + errs = append(errs, errors.New("invalid Memory Size (minimum 4m)")) + } + if len(errs) > 0 { return errors.NewAggregate(errs) } diff --git a/pkg/internal/apis/config/validate_test.go b/pkg/internal/apis/config/validate_test.go index 6d1e7535e4..f3fd62801d 100644 --- a/pkg/internal/apis/config/validate_test.go +++ b/pkg/internal/apis/config/validate_test.go @@ -19,6 +19,8 @@ package config import ( "testing" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/kind/pkg/errors" ) @@ -70,6 +72,21 @@ func TestClusterValidate(t *testing.T) { }(), ExpectErrors: 1, }, + { + Name: "wrong resources constraints node", + Cluster: func() Cluster { + c := Cluster{} + SetDefaults_Cluster(&c) + n, n2 := Node{}, Node{} + SetDefaults_Node(&n) + SetDefaults_Node(&n2) + n.Constraints.Cpus = resource.MustParse("-12") + n.Constraints.Memory = resource.MustParse("1m") + c.Nodes = []Node{n, n2} + return c + }(), + ExpectErrors: 1, + }, } for _, tc := range cases { @@ -151,6 +168,24 @@ func TestNodeValidate(t *testing.T) { }(), ExpectErrors: 1, }, + { + TestName: "Negative CPU constraint", + Node: func() Node { + cfg := newDefaultedNode(ControlPlaneRole) + cfg.Constraints.Cpus = resource.MustParse("-12") + return cfg + }(), + ExpectErrors: 1, + }, + { + TestName: "Minimum value for memory constraint", + Node: func() Node { + cfg := newDefaultedNode(ControlPlaneRole) + cfg.Constraints.Memory = resource.MustParse("2m") + return cfg + }(), + ExpectErrors: 1, + }, } for _, tc := range cases { diff --git a/pkg/internal/apis/config/zz_generated.deepcopy.go b/pkg/internal/apis/config/zz_generated.deepcopy.go index 6ddd65e340..e3c7721b5f 100644 --- a/pkg/internal/apis/config/zz_generated.deepcopy.go +++ b/pkg/internal/apis/config/zz_generated.deepcopy.go @@ -97,6 +97,7 @@ func (in *Node) DeepCopyInto(out *Node) { *out = make([]cri.PortMapping, len(*in)) copy(*out, *in) } + in.Constraints.DeepCopyInto(&out.Constraints) return } @@ -110,6 +111,24 @@ func (in *Node) DeepCopy() *Node { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeResources) DeepCopyInto(out *NodeResources) { + *out = *in + out.Memory = in.Memory.DeepCopy() + out.Cpus = in.Cpus.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeResources. +func (in *NodeResources) DeepCopy() *NodeResources { + if in == nil { + return nil + } + out := new(NodeResources) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PatchJSON6902) DeepCopyInto(out *PatchJSON6902) { *out = *in diff --git a/pkg/internal/cluster/providers/docker/provision.go b/pkg/internal/cluster/providers/docker/provision.go index 0bf4a34c9a..55dbb4196a 100644 --- a/pkg/internal/cluster/providers/docker/provision.go +++ b/pkg/internal/cluster/providers/docker/provision.go @@ -173,9 +173,10 @@ func runArgsForNode(node *config.Node, name string, args []string) []string { args..., ) - // convert mounts and port mappings to container run args + // convert mounts, port mappings and resource constraints to container run args args = append(args, generateMountBindings(node.ExtraMounts...)...) args = append(args, generatePortMappings(node.ExtraPortMappings...)...) + args = append(args, generateNodeConstraints(node.Constraints)...) // finally, specify the image to run return append(args, node.Image) @@ -293,3 +294,21 @@ func generatePortMappings(portMappings ...cri.PortMapping) []string { } return args } + +// generateNodeConstraints converts the nodesConstraints to a list of args for docker +// https://docs.docker.com/config/containers/resource_constraints/ +func generateNodeConstraints(resources config.NodeResources) []string { + var args []string + if resources.Cpus.Sign() > 0 { + args = append(args, fmt.Sprintf("--cpus=%s", resources.Cpus.String())) + } + + if resources.Memory.Sign() > 0 { + args = append(args, fmt.Sprintf("--memory=%s", resources.Memory.String())) + // prevent a container from using swap because we want to emulate a real node + // https://docs.docker.com/config/containers/resource_constraints/#prevent-a-container-from-using-swap + args = append(args, fmt.Sprintf("--memory-swap=%s", resources.Memory.String())) + } + + return args +} diff --git a/site/content/docs/user/quick-start.md b/site/content/docs/user/quick-start.md index ba749d81f4..59a7ab6bb6 100644 --- a/site/content/docs/user/quick-start.md +++ b/site/content/docs/user/quick-start.md @@ -344,6 +344,25 @@ nodes: - role: worker ``` +#### Limit node resources +You can set CPU and memory limits for your cluster nodes in the configuration file. + +```yaml +kind: Cluster +apiVersion: kind.sigs.k8s.io/v1alpha3 +nodes: +# the control plane node +- role: control-plane + constraints: + memory: "200m" + cpu: "2" +- role: worker + constraints: + memory: "100m" + cpu: "1" +``` + + ### Configure kind to use a proxy If you are running kind in an environment that requires a proxy, you may need to configure kind to use it.