Skip to content

Commit

Permalink
feat: Merge CRDs if specification similar (#5)
Browse files Browse the repository at this point in the history
* feat: Merge CRDs if `.spec` is similar
  • Loading branch information
punkwalker authored Jun 13, 2024
1 parent 086826d commit 142a1f2
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 125 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# `karpenter-generate`
This is a simple CLI tool to generate AWS Karpenter Custom Kubernetes Resources (Nodepool & EC2NodeClass) from AWS EKS Managed Nodegroup information. The generated resources can be stored in as a yaml manifest file or can be directly applied to the cluster.
This is a simple CLI tool to generate AWS Karpenter Custom Kubernetes Resources (Nodepool & EC2NodeClass) from AWS EKS Managed Nodegroup information. It will merge similar CRDs if they are equal which reduce number of generated resources. The generated resources can be stored in as a yaml manifest file or can be directly applied to the cluster.

> [!WARNING]
> The tool can only generate ***v1beta*** resources for [Karpenter on AWS](https://karpenter.sh/).
Expand Down Expand Up @@ -37,10 +37,10 @@ Downloaded archive file from release artifacts. Download the archive file from r

| OS | Arch | Download|
| ------ | ------ | ------ |
| Linux | AMD64/x86_64 | [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.5/karpenter-generate_Linux_x86_64.tar.gz)|
| | ARM64| [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.5/karpenter-generate_Linux_arm64.tar.gz)|
| Windows | AMD64/x86_64 | [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.5/karpenter-generate_Windows_x86_64.tar.gz)|
| | ARM64| [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.5/karpenter-generate_Windows_arm64.tar.gz)|
| Linux | AMD64/x86_64 | [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.6/karpenter-generate_Linux_x86_64.tar.gz)|
| | ARM64| [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.6/karpenter-generate_Linux_arm64.tar.gz)|
| Windows | AMD64/x86_64 | [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.6/karpenter-generate_Windows_x86_64.tar.gz)|
| | ARM64| [Link](https://github.com/punkwalker/karpenter-generate/releases/download/v0.0.6/karpenter-generate_Windows_arm64.tar.gz)|

After downloading the archive, extract it and copy the binary/executable to `/usr/local/bin` for Linux. For Windows, run the `karpenter-generate.exe` from extracted folder.

Expand Down
25 changes: 22 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (

"github.com/spf13/cobra"

"github.com/punkwalker/karpenter-generate/pkg/aws"
"github.com/punkwalker/karpenter-generate/pkg/karpenteraws"
"github.com/punkwalker/karpenter-generate/pkg/options"
"github.com/punkwalker/karpenter-generate/pkg/printers"
)

var opts *options.Options
Expand All @@ -16,9 +19,7 @@ var rootCmd = &cobra.Command{
Long: `This is a CLI tool which can be used to generate Karpenter Custom Resources such as
Nodepools and EC2NodeClass from details of EKS Managed Nodegroup. Which will allow seamless migration to Karpenter.`,
SilenceUsage: true,
RunE: func(_ *cobra.Command, _ []string) error {
return run()
},
RunE: run,
}

func init() {
Expand All @@ -36,3 +37,21 @@ func Execute() {
func AddCommand(cmd *cobra.Command) {
rootCmd.AddCommand(cmd)
}

func run(_ *cobra.Command, _ []string) error {
aws.Init(opts)
if err := opts.Parse(); err != nil {
return err
}

printer, err := printers.NewPrinter(printers.Output(opts.Output))
if err != nil {
return err
}

nodePools, nodeClasses, err := karpenteraws.Generate(opts)
if err != nil {
return err
}
return printers.Print(printer, nodePools, nodeClasses)
}
70 changes: 0 additions & 70 deletions cmd/run.go

This file was deleted.

9 changes: 5 additions & 4 deletions pkg/karpenteraws/ec2nodeclass.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
ClusterTagKey string = "kubernetes.io/cluster/"
ALAndBottleRocketDefaultDiskSize int32 = 20
WindowsDefaultDiskSize int32 = 50
TagLabelPattern string = `^(aws:|eksctl|alpha\.eksctl\.io|Name)`
)

var (
Expand Down Expand Up @@ -56,7 +57,8 @@ func (n NodeGroup) AmiID() string {

func (n NodeGroup) NodeClassObjectMeta() metav1.ObjectMeta {
nodeClassannotations := map[string]string{
"generated-by": "karpenter-migrate",
"generated-by": "karpenter-migrate",
"migrate.karpenter.sh/source-nodegroup": n.Name(),
}

return metav1.ObjectMeta{
Expand All @@ -81,9 +83,8 @@ func (n NodeGroup) NodeClassSpec() awskarpenter.EC2NodeClassSpec {

func (n NodeGroup) FilteredTags() map[string]string {
filteredTags := map[string]string{}

for key, val := range n.Tags {
if !strings.HasPrefix(key, "aws:") {
if !tagLabeltoOmmit(key) {
filteredTags[key] = val
}
}
Expand All @@ -92,7 +93,7 @@ func (n NodeGroup) FilteredTags() map[string]string {
if n.CustomLT != nil {
for _, tagspec := range n.CustomLT.TagSpecifications {
for _, tag := range tagspec.Tags {
if !strings.HasPrefix(*tag.Key, "aws:") {
if !tagLabeltoOmmit(*tag.Key) {
filteredTags[*tag.Key] = *tag.Value
}
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/karpenteraws/ec2nodeclass_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ func TestNodeGroup_NodeClassObjectMeta(t *testing.T) {
want: metav1.ObjectMeta{
Name: "my-node-group",
Annotations: map[string]string{
"generated-by": "karpenter-migrate",
"generated-by": "karpenter-migrate",
"migrate.karpenter.sh/source-nodegroup": "my-node-group",
},
},
},
Expand Down
136 changes: 129 additions & 7 deletions pkg/karpenteraws/generate.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package karpenteraws

import (
"fmt"
"regexp"

ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
awskarpenter "github.com/aws/karpenter-provider-aws/pkg/apis/v1beta1"
"github.com/punkwalker/karpenter-generate/pkg/aws"
"github.com/samber/lo"
sigkarpenter "sigs.k8s.io/karpenter/pkg/apis/v1beta1"

"github.com/punkwalker/karpenter-generate/pkg/aws"
"github.com/punkwalker/karpenter-generate/pkg/options"
)

type NodeGroup struct {
Expand All @@ -14,11 +20,21 @@ type NodeGroup struct {
CustomLT *ec2types.ResponseLaunchTemplateData // Custom LT provided to MNG
}

func Generate(nodeGroups *[]ekstypes.Nodegroup) ([]sigkarpenter.NodePool, []awskarpenter.EC2NodeClass, error) {
nodePools := make([]sigkarpenter.NodePool, 0, len(*nodeGroups))
nodeClasses := make([]awskarpenter.EC2NodeClass, 0, len(*nodeGroups))
func Generate(opts *options.Options) ([]sigkarpenter.NodePool, []awskarpenter.EC2NodeClass, error) {
nodeGroups, err := getNodegroups(opts)
if err != nil {
return nil, nil, aws.FormatErrorAsMessageOnly(err)
}

for _, ng := range *nodeGroups {
if len(nodeGroups) == 0 {
return nil, nil, fmt.Errorf("no nodegroups found")
}

npMap := map[string]*sigkarpenter.NodePool{}
ncMap := map[string]*awskarpenter.EC2NodeClass{}
mergedNcMap := map[string]string{}

for _, ng := range nodeGroups {
nodegroup, err := NewNodeGroup(ng)
if err != nil {
return nil, nil, err
Expand All @@ -28,15 +44,25 @@ func Generate(nodeGroups *[]ekstypes.Nodegroup) ([]sigkarpenter.NodePool, []awsk
if err != nil {
return nil, nil, err
}
nodeClasses = append(nodeClasses, ec2Class)

mergeNC(ec2Class, ncMap, &mergedNcMap)

nodePool, err := nodegroup.GetNodePool()
if err != nil {
return nil, nil, err
}
nodePools = append(nodePools, nodePool)

// Merge similar nodepools
mergeNP(nodePool, npMap, mergedNcMap)
}

nodePools := lo.MapToSlice(npMap, func(_ string, v *sigkarpenter.NodePool) sigkarpenter.NodePool {
return *v
})

nodeClasses := lo.MapToSlice(ncMap, func(_ string, v *awskarpenter.EC2NodeClass) awskarpenter.EC2NodeClass {
return *v
})
return nodePools, nodeClasses, nil
}

Expand Down Expand Up @@ -64,3 +90,99 @@ func NewNodeGroup(ng ekstypes.Nodegroup) (*NodeGroup, error) {

return &newNodegroup, nil
}

func getNodegroups(opts *options.Options) ([]ekstypes.Nodegroup, error) {
var nodegroups []ekstypes.Nodegroup
var ngList []string
var err error

eksClient := aws.NewEKSClient()

if opts.NodegroupName != "" {
ngList = []string{opts.NodegroupName}
} else {
ngList, err = eksClient.ListNodegroups(opts.ClusterName)
if err != nil {
return nil, err
}
}

for _, ng := range ngList {
if opts.KarpenterNodegroupName != ng {
nodegroup, err := eksClient.DescribeNodegroup(opts.ClusterName, ng)
if err != nil {
return nil, err
}

if nodegroup.Status != ekstypes.NodegroupStatusActive {
return nil, fmt.Errorf(`nodegroup "%s" is not active, make sure all the nodegroups are in "ACTIVE" state`, ng)
}
nodegroups = append(nodegroups, *nodegroup)
}
}
return nodegroups, nil
}

func mergeNP(newNP sigkarpenter.NodePool, npMap map[string]*sigkarpenter.NodePool, mergedNCMap map[string]string) {
modifiedNP := newNP.DeepCopy()
modifiedNPReqs := modifiedNP.Spec.Template.Spec.Requirements

// Remove instance from Nodepool to check equality
for idx, req := range modifiedNPReqs {
if req.Key == "node.kubernetes.io/instance-type" {
modifiedNP.Spec.Template.Spec.Requirements = append(modifiedNPReqs[:idx], modifiedNPReqs[idx+1:]...)
}
}

if val, ok := mergedNCMap[modifiedNP.Spec.Template.Spec.NodeClassRef.Name]; ok {
modifiedNP.Spec.Template.Spec.NodeClassRef.Name = val
}

npHash := modifiedNP.Hash()
// Add Nodepool to map if nodepool does not exists
if np, exists := (npMap)[npHash]; !exists {
(npMap)[npHash] = &newNP
} else {
// Modify Nodepool if nodepool exists
if val, ok := np.Annotations["migrate.karpenter.sh/merged-nodepools"]; !ok {
np.Annotations["migrate.karpenter.sh/merged-nodepools"] = modifiedNP.Annotations["migrate.karpenter.sh/source-nodegroup"]
} else {
np.Annotations["migrate.karpenter.sh/merged-nodepools"] = fmt.Sprintf("%s,%s", val, modifiedNP.Annotations["migrate.karpenter.sh/source-nodegroup"])
}

// Append Instance Types to existing Nodepool Instance Types
for idx, req := range np.Spec.Template.Spec.Requirements {
if req.Key == "node.kubernetes.io/instance-type" {
for _, reqNew := range newNP.Spec.Template.Spec.Requirements {
if reqNew.Key == "node.kubernetes.io/instance-type" {
newVals := append(req.Values, reqNew.Values...)
np.Spec.Template.Spec.Requirements[idx].Values = lo.Uniq(newVals)
}
}
}
}
}
}

func mergeNC(newNC awskarpenter.EC2NodeClass, ncMap map[string]*awskarpenter.EC2NodeClass, mergedNCMap *map[string]string) {

// Merge similar nodeClasses
ncHash := newNC.Hash()
if nc, exists := ncMap[ncHash]; !exists {
ncMap[ncHash] = &newNC
} else {
// Create record of merged nodeclasses
(*mergedNCMap)[newNC.Name] = nc.Name

if val, ok := nc.Annotations["migrate.karpenter.sh/merged-nodeclasses"]; !ok {
nc.Annotations["migrate.karpenter.sh/merged-nodeclasses"] = newNC.Annotations["migrate.karpenter.sh/source-nodegroup"]
} else {
nc.Annotations["migrate.karpenter.sh/merged-nodeclasses"] = fmt.Sprintf("%s,%s", val, newNC.Annotations["migrate.karpenter.sh/source-nodegroup"])
}
}
}

func tagLabeltoOmmit(key string) bool {
tagLabelRegex := regexp.MustCompile(TagLabelPattern)
return tagLabelRegex.MatchString(key)
}
Loading

0 comments on commit 142a1f2

Please sign in to comment.