Skip to content

Allow running without worker nodes #17544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion cmd/kops/create_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
sshPublicKey := ""
associatePublicIP := false
encryptEtcdStorage := false
nodeCount := int32(0)

cmd := &cobra.Command{
Use: "cluster [CLUSTER]",
Expand All @@ -194,6 +195,10 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
options.EncryptEtcdStorage = &encryptEtcdStorage
}

if cmd.Flag("node-count").Changed {
options.NodeCount = &nodeCount
}

if sshPublicKey != "" {
options.SSHPublicKeys, err = loadSSHPublicKeys(sshPublicKey)
if err != nil {
Expand Down Expand Up @@ -268,7 +273,7 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
cmd.Flags().Int32Var(&options.ControlPlaneCount, "master-count", options.ControlPlaneCount, "Number of control-plane nodes. Defaults to one control-plane node per control-plane-zone")
cmd.Flags().MarkDeprecated("master-count", "use --control-plane-count instead")
cmd.Flags().Int32Var(&options.ControlPlaneCount, "control-plane-count", options.ControlPlaneCount, "Number of control-plane nodes. Defaults to one control-plane node per control-plane-zone")
cmd.Flags().Int32Var(&options.NodeCount, "node-count", options.NodeCount, "Total number of worker nodes. Defaults to one node per zone")
cmd.Flags().Int32Var(&nodeCount, "node-count", 0, "Total number of worker nodes. Defaults to one node per zone")

cmd.Flags().StringVar(&options.Image, "image", options.Image, "Machine image for all instances")
cmd.RegisterFlagCompletionFunc("image", completeInstanceImage)
Expand Down
5 changes: 5 additions & 0 deletions cmd/kops/create_cluster_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ func TestCreateClusterKarpenter(t *testing.T) {
runCreateClusterIntegrationTest(t, "../../tests/integration/create_cluster/karpenter", "v1alpha2")
}

// TestCreateClusterZeroNodes runs kops create cluster --node-count=0
func TestCreateClusterZeroNodes(t *testing.T) {
runCreateClusterIntegrationTest(t, "../../tests/integration/create_cluster/zero-nodes", "v1alpha2")
}

func runCreateClusterIntegrationTest(t *testing.T, srcDir string, version string) {
ctx := context.Background()

Expand Down
4 changes: 0 additions & 4 deletions pkg/apis/kops/validation/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,6 @@ func DeepValidate(c *kops.Cluster, groups []*kops.InstanceGroup, strict bool, vf
return fmt.Errorf("must configure at least one ControlPlane InstanceGroup")
}

if nodeGroupCount == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to feature flag this? I'm OK with not doing so...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I think should be pretty safe without too, as one has to explicitly specify --node-count=0.

return fmt.Errorf("must configure at least one Node InstanceGroup")
}

for _, g := range groups {
errs := CrossValidateInstanceGroup(g, c, cloud, strict)

Expand Down
76 changes: 76 additions & 0 deletions tests/integration/create_cluster/zero-nodes/expected-v1alpha2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
apiVersion: kops.k8s.io/v1alpha2
kind: Cluster
metadata:
creationTimestamp: "2017-01-01T00:00:00Z"
name: minimal.example.com
spec:
api:
loadBalancer:
class: Network
type: Public
authorization:
rbac: {}
channel: stable
cloudProvider: aws
configBase: memfs://tests/minimal.example.com
etcdClusters:
- cpuRequest: 200m
etcdMembers:
- encryptedVolume: true
instanceGroup: control-plane-us-test-1a
name: a
manager:
backupRetentionDays: 90
memoryRequest: 100Mi
name: main
- cpuRequest: 100m
etcdMembers:
- encryptedVolume: true
instanceGroup: control-plane-us-test-1a
name: a
manager:
backupRetentionDays: 90
memoryRequest: 100Mi
name: events
iam:
allowContainerRegistry: true
legacy: false
kubelet:
anonymousAuth: false
kubernetesApiAccess:
- 0.0.0.0/0
- ::/0
kubernetesVersion: v1.32.0
networkCIDR: 172.20.0.0/16
networking:
cni: {}
nonMasqueradeCIDR: 100.64.0.0/10
sshAccess:
- 0.0.0.0/0
- ::/0
subnets:
- cidr: 172.20.0.0/16
name: us-test-1a
type: Public
zone: us-test-1a
topology:
dns:
type: None

---

apiVersion: kops.k8s.io/v1alpha2
kind: InstanceGroup
metadata:
creationTimestamp: "2017-01-01T00:00:00Z"
labels:
kops.k8s.io/cluster: minimal.example.com
name: control-plane-us-test-1a
spec:
image: 099720109477/ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-20250610
machineType: m3.medium
maxSize: 1
minSize: 1
role: Master
subnets:
- us-test-1a
7 changes: 7 additions & 0 deletions tests/integration/create_cluster/zero-nodes/options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ClusterName: minimal.example.com
Zones:
- us-test-1a
CloudProvider: aws
Networking: cni
KubernetesVersion: v1.32.0
NodeCount: 0
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ spec:
- key: "CriticalAddonsOnly"
operator: "Exists"
{{- end }}
{{- if KarpenterEnabled }}
{{- if or IsControlPlaneOnly KarpenterEnabled }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want a helper like CoreDNSOnControlPlane , but not a big deal

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to reuse this, it has nothing to do with CoreDNS specifically.

- key: node-role.kubernetes.io/master
operator: Exists
- key: node-role.kubernetes.io/control-plane
Expand Down
5 changes: 4 additions & 1 deletion upup/pkg/fi/cloudup/deepvalidate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ func TestDeepValidate_NoNodeZones(t *testing.T) {
c := buildDefaultCluster(t)
var groups []*kopsapi.InstanceGroup
groups = append(groups, buildMinimalMasterInstanceGroup("subnet-us-test-1a"))
expectErrorFromDeepValidate(t, c, groups, "must configure at least one Node InstanceGroup")
err := validation.DeepValidate(c, groups, true, vfs.Context, nil)
if err != nil {
t.Fatalf("Expected no error from DeepValidate, got %v", err)
}
}

func TestDeepValidate_NoMasterZones(t *testing.T) {
Expand Down
32 changes: 22 additions & 10 deletions upup/pkg/fi/cloudup/new_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ type NewClusterOptions struct {

// NodeCount is the number of nodes to create. Defaults to leaving the count unspecified
// on the InstanceGroup, which results in a count of 2.
NodeCount int32
NodeCount *int32
// Bastion enables the creation of a Bastion instance.
Bastion bool
// BastionLoadBalancerType is the bastion loadbalancer type to use; "public" or "internal".
Expand Down Expand Up @@ -1040,9 +1040,15 @@ func setupNodes(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetsMap m
var nodes []*api.InstanceGroup

if featureflag.AWSSingleNodesInstanceGroup.Enabled() && cloudProvider == api.CloudProviderAWS && len(opt.SubnetIDs) == 0 {
nodeCount := opt.NodeCount
if nodeCount == 0 {
var nodeCount int32
if opt.NodeCount == nil {
nodeCount = 1
} else {
nodeCount = fi.ValueOf(opt.NodeCount)
if nodeCount == 0 {
// If the node count is 0, there's nothing to do
return nil, nil
}
}

g := &api.InstanceGroup{}
Expand Down Expand Up @@ -1078,19 +1084,25 @@ func setupNodes(opt *NewClusterOptions, cluster *api.Cluster, zoneToSubnetsMap m

// The node count is the number of zones unless explicitly set
// We then divvy up amongst the zones
numZones := len(opt.Zones)
nodeCount := opt.NodeCount
if nodeCount == 0 {
// If node count is not specified, default to the number of zones
nodeCount = int32(numZones)
numZones := int32(len(opt.Zones))
var nodeCount int32
if opt.NodeCount == nil {
// If the node count is not specified, default to the number of zones
nodeCount = numZones
} else {
nodeCount = fi.ValueOf(opt.NodeCount)
if nodeCount == 0 {
// If the node count is 0, there's nothing to do
return nil, nil
}
}

countPerIG := nodeCount / int32(numZones)
remainder := int(nodeCount) % numZones
remainder := nodeCount % numZones

for i, zone := range opt.Zones {
count := countPerIG
if i < remainder {
if i < int(remainder) {
count++
}

Expand Down
18 changes: 18 additions & 0 deletions upup/pkg/fi/cloudup/template_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ func (tf *TemplateFunctions) AddTo(dest template.FuncMap, secretStore fi.SecretS

dest["ParseTaint"] = util.ParseTaint

dest["IsControlPlaneOnly"] = tf.IsControlPlaneOnly

dest["KarpenterEnabled"] = func() bool {
return cluster.Spec.Karpenter != nil && cluster.Spec.Karpenter.Enabled
}
Expand Down Expand Up @@ -511,6 +513,22 @@ func (tf *TemplateFunctions) HasHighlyAvailableControlPlane() bool {
return false
}

// IsControlPlaneOnly returns true if the cluster has only control plane node(s). False otherwise.
func (tf *TemplateFunctions) IsControlPlaneOnly() bool {
var cp, wn int
for _, ig := range tf.InstanceGroups {
switch ig.Spec.Role {
case kops.InstanceGroupRoleControlPlane:
cp++
case kops.InstanceGroupRoleNode:
wn++
default:
// Ignore Bastion and APIServer
}
}
return cp > 0 && wn == 0
}

// CloudControllerConfigArgv returns the args to external cloud controller
func (tf *TemplateFunctions) CloudControllerConfigArgv() ([]string, error) {
cluster := tf.Cluster
Expand Down
Loading