Skip to content

Commit bc46950

Browse files
authored
ASG AllocationStrategy support, subnet discovery (#55)
1 parent bfac310 commit bc46950

File tree

6 files changed

+171
-25
lines changed

6 files changed

+171
-25
lines changed

operator/config/data-plane-crd.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,35 @@ spec:
3535
type: object
3636
spec:
3737
properties:
38+
allocationStrategy:
39+
description: AllocationStrategy helps user define the strategy to
40+
provision worker nodes in EC2, defaults to "lowest-price"
41+
type: string
3842
clusterName:
43+
description: ClusterName is used to connect the worker nodes to a
44+
control plane clusterName.
3945
type: string
46+
instanceTypes:
47+
description: InstanceTypes is an optional field thats lets user specify
48+
the instance types for worker nodes, defaults to instance types
49+
"t2.xlarge", "t3.xlarge" or "t3a.xlarge"
50+
items:
51+
type: string
52+
type: array
4053
nodeCount:
54+
description: NodeCount is the desired number of worker nodes for this
55+
dataplane.
4156
type: integer
57+
subnetSelector:
58+
additionalProperties:
59+
type: string
60+
description: SubnetSelector lets user define label key and values
61+
for kit to select the subnets for worker nodes. It can contain key:value
62+
to select subnets with particular label, or a specific key:"*" to
63+
select all subnets with a specific key. If no selector is provided,
64+
worker nodes are provisioned in the same subnet as control plane
65+
nodes.
66+
type: object
4267
type: object
4368
status:
4469
properties:

operator/docs/examples/dataplane.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@ metadata:
44
name: example-nodes
55
spec:
66
clusterName: example # Desired Cluster Name
7-
nodeCount: 1
7+
nodeCount: 1
8+
subnetSelector:
9+
kubernetes.io/cluster/kit-management-cluster: "*"
10+
instanceTypes:
11+
- c4.xlarge
12+
- c5.xlarge
13+
- c4.4xlarge
14+
- c5.4xlarge

operator/pkg/apis/dataplane/v1alpha1/dataplane.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ type DataPlaneList struct {
3838
}
3939

4040
type DataPlaneSpec struct {
41+
// ClusterName is used to connect the worker nodes to a control plane clusterName.
4142
ClusterName string `json:"clusterName,omitempty"`
42-
NodeCount int `json:"nodeCount,omitempty"`
43+
// NodeCount is the desired number of worker nodes for this dataplane.
44+
NodeCount int `json:"nodeCount,omitempty"`
45+
// SubnetSelector lets user define label key and values for kit to select
46+
// the subnets for worker nodes. It can contain key:value to select subnets
47+
// with particular label, or a specific key:"*" to select all subnets with a
48+
// specific key. If no selector is provided, worker nodes are
49+
// provisioned in the same subnet as control plane nodes.
50+
// +optional
51+
SubnetSelector map[string]string `json:"subnetSelector,omitempty"`
52+
// InstanceTypes is an optional field thats lets user specify the instance
53+
// types for worker nodes, defaults to instance types "t2.xlarge", "t3.xlarge" or "t3a.xlarge"
54+
// +optional
55+
InstanceTypes []string `json:"instanceTypes,omitempty"`
56+
// AllocationStrategy helps user define the strategy to provision worker nodes in EC2,
57+
// defaults to "lowest-price"
58+
// +optional
59+
AllocationStrategy string `json:"allocationStrategy,omitempty"`
4360
}

operator/pkg/apis/dataplane/v1alpha1/dataplane_defaults.go

+6
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,10 @@ func (c *DataPlane) SetDefaults(ctx context.Context) {
2424

2525
// SetDefaults for the DataPlaneSpec, cascading to all subspecs
2626
func (s *DataPlaneSpec) SetDefaults(ctx context.Context) {
27+
if s.AllocationStrategy == "" {
28+
s.AllocationStrategy = "lowest-price"
29+
}
30+
if len(s.InstanceTypes) == 0 {
31+
s.InstanceTypes = []string{"t2.xlarge", "t3.xlarge", "t3a.xlarge"}
32+
}
2733
}

operator/pkg/apis/dataplane/v1alpha1/zz_generated.deepcopy.go

+13-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

operator/pkg/awsprovider/instances/reconciler.go

+101-22
Original file line numberDiff line numberDiff line change
@@ -45,25 +45,22 @@ func NewController(ec2api *awsprovider.EC2, autoscaling *awsprovider.AutoScaling
4545
}
4646

4747
func (c *Controller) Reconcile(ctx context.Context, dataplane *v1alpha1.DataPlane) error {
48-
privateSubnets, err := c.getPrivateSubnetsFor(ctx, dataplane.Spec.ClusterName)
49-
if err != nil {
50-
return fmt.Errorf("getting private subnet for %s, %w", dataplane.Spec.ClusterName, err)
51-
}
52-
if len(privateSubnets) == 0 {
53-
return fmt.Errorf("failed to find private subnets for dataplane")
54-
}
5548
asg, err := c.getAutoScalingGroup(ctx, AutoScalingGroupNameFor(dataplane.Spec.ClusterName))
5649
if err != nil {
5750
return fmt.Errorf("getting auto scaling group for %v, %w", dataplane.Spec.ClusterName, err)
5851
}
5952
if asg == nil {
60-
if err := c.createAutoScalingGroup(ctx, dataplane, privateSubnets); err != nil {
53+
if err := c.createAutoScalingGroup(ctx, dataplane); err != nil {
6154
return fmt.Errorf("creating auto scaling group for %v, %w", dataplane.Spec.ClusterName, err)
6255
}
6356
zap.S().Infof("[%s] Created autoscaling group", dataplane.Spec.ClusterName)
6457
return nil
6558
}
66-
if err := c.updateAutoScalingGroup(ctx, dataplane, asg, privateSubnets); err != nil {
59+
if asg.Status != nil && *asg.Status == "Delete in progress" {
60+
// there are scenarios if you delete ASG and recreate quickly ASG might still be getting deleted
61+
return fmt.Errorf("ASG %v deletion in progress", asg.AutoScalingGroupName)
62+
}
63+
if err := c.updateAutoScalingGroup(ctx, dataplane, asg); err != nil {
6764
return fmt.Errorf("updating auto scaling group %v, %w", AutoScalingGroupNameFor(dataplane.Spec.ClusterName), err)
6865
}
6966
return nil
@@ -79,31 +76,65 @@ func (c *Controller) Finalize(ctx context.Context, dataplane *v1alpha1.DataPlane
7976
return nil
8077
}
8178

82-
func (c *Controller) updateAutoScalingGroup(ctx context.Context, dataplane *v1alpha1.DataPlane, asg *autoscaling.Group, subnets []string) error {
79+
func (c *Controller) updateAutoScalingGroup(ctx context.Context, dataplane *v1alpha1.DataPlane, asg *autoscaling.Group) error {
80+
subnets, err := c.subnetsFor(ctx, dataplane)
81+
if err != nil {
82+
return fmt.Errorf("getting private subnet for %s, %w", dataplane.Spec.ClusterName, err)
83+
}
84+
if len(subnets) == 0 {
85+
return fmt.Errorf("failed to find private subnets for dataplane")
86+
}
8387
if functional.ValidateAll(
8488
func() bool { return asg != nil },
8589
func() bool {
8690
return functional.StringsMatch(strings.Split(ptr.StringValue(asg.VPCZoneIdentifier), ","), subnets)
8791
},
88-
func() bool { return ptr.Int64Value(asg.DesiredCapacity) == int64(dataplane.Spec.NodeCount) }) {
92+
func() bool { return ptr.Int64Value(asg.DesiredCapacity) == int64(dataplane.Spec.NodeCount) },
93+
func() bool {
94+
return functional.StringsMatch(
95+
parseOverridesFromASG(asg.MixedInstancesPolicy.LaunchTemplate.Overrides),
96+
parseOverridesFromASG(instanceTypes(dataplane.Spec.InstanceTypes)),
97+
)
98+
}) {
8999
return nil
90100
}
91-
_, err := c.autoscaling.UpdateAutoScalingGroupWithContext(ctx, &autoscaling.UpdateAutoScalingGroupInput{
101+
zap.S().Infof("[%v] updating ASG %v", dataplane.Spec.ClusterName, *asg.AutoScalingGroupName)
102+
_, err = c.autoscaling.UpdateAutoScalingGroupWithContext(ctx, &autoscaling.UpdateAutoScalingGroupInput{
92103
AutoScalingGroupName: ptr.String(AutoScalingGroupNameFor(dataplane.Spec.ClusterName)),
93104
DesiredCapacity: ptr.Int64(int64(dataplane.Spec.NodeCount)),
94105
VPCZoneIdentifier: ptr.String(strings.Join(subnets, ",")),
106+
MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{
107+
LaunchTemplate: &autoscaling.LaunchTemplate{
108+
Overrides: instanceTypes(dataplane.Spec.InstanceTypes),
109+
},
110+
},
95111
})
96112
return err
97113
}
98114

99-
func (c *Controller) createAutoScalingGroup(ctx context.Context, dataplane *v1alpha1.DataPlane, subnets []string) error {
100-
_, err := c.autoscaling.CreateAutoScalingGroupWithContext(ctx, &autoscaling.CreateAutoScalingGroupInput{
115+
func (c *Controller) createAutoScalingGroup(ctx context.Context, dataplane *v1alpha1.DataPlane) error {
116+
subnets, err := c.subnetsFor(ctx, dataplane)
117+
if err != nil {
118+
return fmt.Errorf("getting private subnet for %s, %w", dataplane.Spec.ClusterName, err)
119+
}
120+
if len(subnets) == 0 {
121+
return fmt.Errorf("failed to find private subnets for dataplane")
122+
}
123+
_, err = c.autoscaling.CreateAutoScalingGroupWithContext(ctx, &autoscaling.CreateAutoScalingGroupInput{
101124
AutoScalingGroupName: ptr.String(AutoScalingGroupNameFor(dataplane.Spec.ClusterName)),
102125
DesiredCapacity: ptr.Int64(int64(dataplane.Spec.NodeCount)),
103126
MaxSize: ptr.Int64(int64(1000)),
104127
MinSize: ptr.Int64(int64(0)),
105-
LaunchTemplate: &autoscaling.LaunchTemplateSpecification{
106-
LaunchTemplateName: ptr.String(launchtemplate.TemplateName(dataplane.Spec.ClusterName)),
128+
MixedInstancesPolicy: &autoscaling.MixedInstancesPolicy{
129+
InstancesDistribution: &autoscaling.InstancesDistribution{
130+
OnDemandAllocationStrategy: ptr.String(dataplane.Spec.AllocationStrategy),
131+
},
132+
LaunchTemplate: &autoscaling.LaunchTemplate{
133+
LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{
134+
LaunchTemplateName: ptr.String(launchtemplate.TemplateName(dataplane.Spec.ClusterName)),
135+
},
136+
Overrides: instanceTypes(dataplane.Spec.InstanceTypes),
137+
},
107138
},
108139
VPCZoneIdentifier: ptr.String(strings.Join(subnets, ",")),
109140
Tags: generateAutoScalingTags(dataplane.Spec.ClusterName),
@@ -127,14 +158,19 @@ func (c *Controller) getAutoScalingGroup(ctx context.Context, groupName string)
127158
return output.AutoScalingGroups[0], nil
128159
}
129160

130-
func (c *Controller) getPrivateSubnetsFor(ctx context.Context, clusterName string) ([]string, error) {
131-
instanceIDs, err := c.instances.ControlPlaneInstancesFor(ctx, clusterName)
161+
func (c *Controller) subnetsFor(ctx context.Context, dataplane *v1alpha1.DataPlane) ([]string, error) {
162+
// Discover subnets provided as part of the subnetSelector in DP spec.
163+
if len(dataplane.Spec.SubnetSelector) != 0 {
164+
return c.subnetsForSelector(ctx, dataplane.Spec.SubnetSelector)
165+
}
166+
// If subnetSelector is not provided fallback on control plane instance subnets
167+
instanceIDs, err := c.instances.ControlPlaneInstancesFor(ctx, dataplane.Spec.ClusterName)
132168
if err != nil {
133169
return nil, err
134170
}
135-
subnetIDs, err := c.getSubnetIDsFor(ctx, instanceIDs)
171+
subnetIDs, err := c.subnetsForInstances(ctx, instanceIDs)
136172
if err != nil {
137-
return nil, fmt.Errorf("getting subnet for %s, %w", clusterName, err)
173+
return nil, fmt.Errorf("getting subnet for %s, %w", dataplane.Spec.ClusterName, err)
138174
}
139175
return c.filterPrivateSubnets(ctx, subnetIDs)
140176
}
@@ -156,7 +192,34 @@ func (c *Controller) filterPrivateSubnets(ctx context.Context, ids []*string) ([
156192
return result, nil
157193
}
158194

159-
func (c *Controller) getSubnetIDsFor(ctx context.Context, instanceIDs []string) ([]*string, error) {
195+
func (c *Controller) subnetsForSelector(ctx context.Context, selector map[string]string) ([]string, error) {
196+
filters := []*ec2.Filter{}
197+
// Filter by selector
198+
for key, value := range selector {
199+
if value == "*" {
200+
filters = append(filters, &ec2.Filter{
201+
Name: aws.String("tag-key"),
202+
Values: []*string{aws.String(key)},
203+
})
204+
} else {
205+
filters = append(filters, &ec2.Filter{
206+
Name: aws.String(fmt.Sprintf("tag:%s", key)),
207+
Values: []*string{aws.String(value)},
208+
})
209+
}
210+
}
211+
output, err := c.ec2api.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{Filters: filters})
212+
if err != nil {
213+
return nil, fmt.Errorf("describing subnets %+v, %w", filters, err)
214+
}
215+
result := []string{}
216+
for _, o := range output.Subnets {
217+
result = append(result, *o.SubnetId)
218+
}
219+
return result, nil
220+
}
221+
222+
func (c *Controller) subnetsForInstances(ctx context.Context, instanceIDs []string) ([]*string, error) {
160223
requestIds := []*string{}
161224
for _, instanceID := range instanceIDs {
162225
requestIds = append(requestIds, ptr.String(instanceID))
@@ -191,7 +254,23 @@ func generateAutoScalingTags(clusterName string) []*autoscaling.Tag {
191254
PropagateAtLaunch: aws.Bool(true),
192255
}, {
193256
Key: aws.String("Name"),
194-
Value: aws.String("auto-scaling-group"),
257+
Value: aws.String(fmt.Sprintf("%s-dataplane-nodes", clusterName)),
195258
PropagateAtLaunch: aws.Bool(true),
196259
}}
197260
}
261+
262+
func instanceTypes(overrides []string) []*autoscaling.LaunchTemplateOverrides {
263+
result := []*autoscaling.LaunchTemplateOverrides{}
264+
for _, override := range overrides {
265+
result = append(result, &autoscaling.LaunchTemplateOverrides{InstanceType: ptr.String(override)})
266+
}
267+
return result
268+
}
269+
270+
func parseOverridesFromASG(overrides []*autoscaling.LaunchTemplateOverrides) []string {
271+
result := []string{}
272+
for _, override := range overrides {
273+
result = append(result, ptr.StringValue(override.InstanceType))
274+
}
275+
return result
276+
}

0 commit comments

Comments
 (0)