Skip to content

Commit b38bcfe

Browse files
authored
[CASCL-1386] (6/11) Extract shared EC2 instance-ID helper to common/aws (#3173)
* [CASCL-1386] Add evict-legacy-nodes command skeleton Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands. * [CASCL-1386] Implement evict-legacy-nodes execution plan building Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands. * [CASCL-1386] Implement evict-legacy-nodes plan display and confirmation Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands. * [CASCL-1386] Implement evict-legacy-nodes preflight warnings Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands. * [CASCL-1386] Implement cluster-autoscaler scale-down for evict-legacy-nodes Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands. * [CASCL-1386] Extract shared EC2 instance-ID helper to common/aws Part of a stack splitting #3026 (too large to review in one piece) into small pieces that each build and pass tests on their own. The command is fully functional only once the whole stack lands.
1 parent 987f3af commit b38bcfe

4 files changed

Lines changed: 81 additions & 17 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package aws
2+
3+
import (
4+
"regexp"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
)
8+
9+
// awsProviderIDRegexp matches the AWS provider ID for EC2-backed nodes.
10+
// Format: aws:///<az>/i-<hex> (e.g. aws:///us-east-1a/i-0abc123def456789).
11+
// Fargate nodes use a different shape (aws:///<az>/fargate-ip-...) and must
12+
// therefore be classified by label before reaching this regex.
13+
var awsProviderIDRegexp = regexp.MustCompile(`^aws:///[^/]+/(i-[0-9a-f]+)$`)
14+
15+
// LabelEKSNodegroup is the label EKS stamps on every node that belongs to a
16+
// managed node group. The label value is the node group name. Exposed as a
17+
// constant so every consumer (classifier, evict-legacy-nodes, future code)
18+
// references the same string.
19+
const LabelEKSNodegroup = "eks.amazonaws.com/nodegroup"
20+
21+
// ExtractEC2InstanceID returns the EC2 instance ID (i-...) from a Node's
22+
// providerID, or false when the providerID is not an EC2 instance (Fargate
23+
// uses `aws:///<az>/fargate-ip-...`, GCP/Azure use entirely different shapes,
24+
// etc.). Lives here in `common/aws` so both `common/clusterinfo` (which
25+
// imports `common/karpenter`) and `common/karpenter` (which classifies its
26+
// own nodes) can use it without creating an import cycle.
27+
func ExtractEC2InstanceID(node *corev1.Node) (string, bool) {
28+
if node == nil {
29+
return "", false
30+
}
31+
m := awsProviderIDRegexp.FindStringSubmatch(node.Spec.ProviderID)
32+
if len(m) != 2 {
33+
return "", false
34+
}
35+
return m[1], true
36+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package aws
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
corev1 "k8s.io/api/core/v1"
8+
)
9+
10+
func TestExtractEC2InstanceID(t *testing.T) {
11+
for _, tc := range []struct {
12+
name string
13+
provider string
14+
wantID string
15+
wantOK bool
16+
}{
17+
{name: "ec2", provider: "aws:///eu-west-3a/i-0123456789abcdef0", wantID: "i-0123456789abcdef0", wantOK: true},
18+
{name: "ec2 short id", provider: "aws:///us-east-1b/i-abc123", wantID: "i-abc123", wantOK: true},
19+
{name: "fargate", provider: "aws:///eu-west-3a/fargate-ip-10-0-1-2", wantOK: false},
20+
{name: "gcp", provider: "gce://project/zone/instance", wantOK: false},
21+
{name: "empty", provider: "", wantOK: false},
22+
{name: "missing prefix", provider: "i-0123456789abcdef0", wantOK: false},
23+
{name: "missing AZ", provider: "aws:////i-0123456789abcdef0", wantOK: false},
24+
} {
25+
t.Run(tc.name, func(t *testing.T) {
26+
node := &corev1.Node{Spec: corev1.NodeSpec{ProviderID: tc.provider}}
27+
id, ok := ExtractEC2InstanceID(node)
28+
assert.Equal(t, tc.wantOK, ok)
29+
if tc.wantOK {
30+
assert.Equal(t, tc.wantID, id)
31+
}
32+
})
33+
}
34+
t.Run("nil node", func(t *testing.T) {
35+
_, ok := ExtractEC2InstanceID(nil)
36+
assert.False(t, ok)
37+
})
38+
}

cmd/kubectl-datadog/autoscaling/cluster/common/clusterinfo/classify.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log"
77
"maps"
8-
"regexp"
98
"strings"
109
"time"
1110

@@ -36,11 +35,6 @@ import (
3635
// ValidationError at the API.
3736
const describeASGInstancesMaxIDs = 50
3837

39-
// awsProviderIDRegexp matches the AWS provider ID for EC2-backed nodes.
40-
// Format: aws:///<az>/i-<hex>. Fargate nodes use a different shape and
41-
// must therefore be classified by label before reaching this regex.
42-
var awsProviderIDRegexp = regexp.MustCompile(`^aws:///[^/]+/(i-[0-9a-f]+)$`)
43-
4438
// nodePoolDatadogCreatedLabel is the label set by every Datadog autoscaling
4539
// product (kubectl-datadog AND the cluster agent) on the NodePools they
4640
// manage. Broader than the AND-pair `app.kubernetes.io/managed-by:
@@ -173,10 +167,9 @@ func classifyByLabels(ctx context.Context, k8sClient kubernetes.Interface, farga
173167
return nil
174168
}
175169

176-
matches := awsProviderIDRegexp.FindStringSubmatch(node.Spec.ProviderID)
177-
if len(matches) == 2 {
170+
if id, ok := commonaws.ExtractEC2InstanceID(node); ok {
178171
candidates = append(candidates, asgCandidate{
179-
instanceID: matches[1],
172+
instanceID: id,
180173
nodeName: node.Name,
181174
})
182175
} else {

cmd/kubectl-datadog/autoscaling/cluster/common/karpenter/fromnodes.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log"
77
"maps"
8-
"regexp"
98
"slices"
109
"strings"
1110

@@ -17,11 +16,9 @@ import (
1716
"k8s.io/apimachinery/pkg/runtime"
1817
"k8s.io/client-go/kubernetes"
1918
"k8s.io/client-go/tools/pager"
20-
)
2119

22-
// awsProviderIDRegexp matches the AWS provider ID format for EC2 instances.
23-
// Format: aws:///ZONE/INSTANCE_ID (e.g., aws:///us-east-1a/i-0abc123def456789)
24-
var awsProviderIDRegexp = regexp.MustCompile(`^aws:///[^/]+/(i-[0-9a-f]+)$`)
20+
commonaws "github.com/DataDog/datadog-operator/cmd/kubectl-datadog/autoscaling/cluster/common/aws"
21+
)
2522

2623
// ec2DescribeBatchSize bounds the number of instance IDs we hand to a single
2724
// ec2:DescribeInstances / ec2:DescribeImages call. The K8s pager streams
@@ -62,12 +59,12 @@ func GetNodesProperties(ctx context.Context, clientset *kubernetes.Clientset, ec
6259
if _, isKarpenter := node.Labels["karpenter.k8s.aws/ec2nodeclass"]; isKarpenter {
6360
return nil
6461
}
65-
matches := awsProviderIDRegexp.FindStringSubmatch(node.Spec.ProviderID)
66-
if len(matches) != 2 {
62+
id, ok := commonaws.ExtractEC2InstanceID(node)
63+
if !ok {
6764
log.Printf("Skipping node %s with unexpected provider ID: %s", node.Name, node.Spec.ProviderID)
6865
return nil
6966
}
70-
pending[matches[1]] = pendingNode{labels: node.Labels, taints: node.Spec.Taints}
67+
pending[id] = pendingNode{labels: node.Labels, taints: node.Spec.Taints}
7168
if len(pending) >= ec2DescribeBatchSize {
7269
return flush()
7370
}

0 commit comments

Comments
 (0)