diff --git a/integration/matchers/json.go b/integration/matchers/json.go index 37c793c95b..b9004b7542 100644 --- a/integration/matchers/json.go +++ b/integration/matchers/json.go @@ -3,8 +3,7 @@ package matchers import ( "github.com/onsi/gomega/types" "github.com/pkg/errors" - - "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/actions/nodegroup" "encoding/json" ) @@ -28,9 +27,9 @@ func (matcher *jsonNodeGroupMatcher) Match(actual interface{}) (success bool, er if !ok { return false, errors.Wrapf(err, "BeNodeGroupsWithNamesWhich matcher expects a string") } - ngSummaries := []manager.NodeGroupSummary{} + ngSummaries := []nodegroup.Summary{} if err := json.Unmarshal([]byte(rawJSON), &ngSummaries); err != nil { - return false, errors.Wrapf(err, "BeNodeGroupsWithNamesWhich matcher expects a NodeGroupSummary JSON array") + return false, errors.Wrapf(err, "BeNodeGroupsWithNamesWhich matcher expects a Summary JSON array") } ngNames := extractNames(ngSummaries) for _, m := range matcher.matchers { @@ -43,7 +42,7 @@ func (matcher *jsonNodeGroupMatcher) Match(actual interface{}) (success bool, er return true, nil } -func extractNames(ngSummaries []manager.NodeGroupSummary) []string { +func extractNames(ngSummaries []nodegroup.Summary) []string { ngNames := make([]string, len(ngSummaries)) for i, ngSummary := range ngSummaries { ngNames[i] = ngSummary.Name diff --git a/pkg/actions/nodegroup/get.go b/pkg/actions/nodegroup/get.go index 3655a9a590..ef955b9c4e 100644 --- a/pkg/actions/nodegroup/get.go +++ b/pkg/actions/nodegroup/get.go @@ -1,9 +1,13 @@ package nodegroup import ( + "fmt" "strconv" "strings" + "time" + cfn "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/tidwall/gjson" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/aws/aws-sdk-go/aws" @@ -12,27 +16,64 @@ import ( "github.com/aws/aws-sdk-go/service/eks" awseks "github.com/aws/aws-sdk-go/service/eks" "github.com/kris-nova/logger" - "github.com/pkg/errors" "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/cfn/outputs" kubewrapper "github.com/weaveworks/eksctl/pkg/kubernetes" ) -func (m *Manager) GetAll() ([]*manager.NodeGroupSummary, error) { - summaries, err := m.stackManager.GetUnmanagedNodeGroupSummaries("") +const ( + imageIDPath = "Resources.NodeGroupLaunchTemplate.Properties.LaunchTemplateData.ImageId" + resourcesRootPath = "Resources" +) + +// Summary represents a summary of a nodegroup stack +type Summary struct { + StackName string + Cluster string + Name string + Status string + MaxSize int + MinSize int + DesiredCapacity int + InstanceType string + ImageID string + CreationTime time.Time + NodeInstanceRoleARN string + AutoScalingGroupName string + Version string + NodeGroupType api.NodeGroupType `json:"Type"` +} + +func (m *Manager) GetAll() ([]*Summary, error) { + unmanagedSummaries, err := m.getUnmanagedSummaries() if err != nil { - return nil, errors.Wrap(err, "getting nodegroup stack summaries") + return nil, err } - for _, summary := range summaries { - if summary.DesiredCapacity > 0 { - summary.Version, err = kubewrapper.GetNodegroupKubernetesVersion(m.clientSet.CoreV1().Nodes(), summary.Name) - if err != nil { - return nil, errors.Wrap(err, "getting nodegroup's kubernetes version") - } - } + managedSummaries, err := m.getManagedSummaries() + if err != nil { + return nil, err } + return append(unmanagedSummaries, managedSummaries...), nil +} + +func (m *Manager) Get(name string) (*Summary, error) { + summary, err := m.getUnmanagedSummary(name) + if err != nil { + return nil, fmt.Errorf("getting nodegroup stack summaries: %w", err) + } + + if summary != nil { + return summary, nil + } + + return m.getManagedSummary(name) +} + +func (m *Manager) getManagedSummaries() ([]*Summary, error) { + var summaries []*Summary managedNodeGroups, err := m.ctl.Provider.EKS().ListNodegroups(&eks.ListNodegroupsInput{ ClusterName: aws.String(m.cfg.Metadata.Name), }) @@ -47,7 +88,7 @@ func (m *Manager) GetAll() ([]*manager.NodeGroupSummary, error) { stack = &cloudformation.Stack{} } - summary, err := m.makeManagedNGSummary(*ngName) + summary, err := m.getManagedSummary(*ngName) if err != nil { return nil, err } @@ -58,27 +99,183 @@ func (m *Manager) GetAll() ([]*manager.NodeGroupSummary, error) { return summaries, nil } -func (m *Manager) Get(name string) (*manager.NodeGroupSummary, error) { - summaries, err := m.stackManager.GetUnmanagedNodeGroupSummaries(name) +func (m *Manager) getUnmanagedSummaries() ([]*Summary, error) { + stacks, err := m.stackManager.DescribeNodeGroupStacks() + if err != nil { + return nil, fmt.Errorf("getting nodegroup stacks: %w", err) + } + + // Create an empty array here so that an object is returned rather than null + summaries := make([]*Summary, 0) + for _, s := range stacks { + summary, err := m.unmanagedStackToSummary(s) + if err != nil { + return nil, err + } + if summary != nil { + summaries = append(summaries, summary) + } + } + + return summaries, nil +} + +func (m *Manager) getUnmanagedSummary(name string) (*Summary, error) { + stack, err := m.stackManager.DescribeNodeGroupStack(name) + if err != nil { + return nil, err + } + + return m.unmanagedStackToSummary(stack) +} + +func (m *Manager) unmanagedStackToSummary(s *manager.Stack) (*Summary, error) { + nodeGroupType, err := manager.GetNodeGroupType(s.Tags) + if err != nil { + return nil, err + } + + if nodeGroupType != api.NodeGroupTypeUnmanaged { + return nil, nil + } + + ngPaths, err := getNodeGroupPaths(s.Tags) + if err != nil { + return nil, err + } + + summary, err := m.mapStackToNodeGroupSummary(s, ngPaths) + + if err != nil { + return nil, fmt.Errorf("mapping stack to nodegroup summary: %w", err) + } + summary.NodeGroupType = api.NodeGroupTypeUnmanaged + + asgName, err := m.stackManager.GetUnmanagedNodeGroupAutoScalingGroupName(s) + if err != nil { + return nil, fmt.Errorf("getting autoscalinggroupname: %w", err) + } + + summary.AutoScalingGroupName = asgName + + scalingGroup, err := m.stackManager.GetAutoScalingGroupDesiredCapacity(asgName) + if err != nil { + return nil, fmt.Errorf("getting autoscalinggroup desired capacity: %w", err) + } + summary.DesiredCapacity = int(*scalingGroup.DesiredCapacity) + summary.MinSize = int(*scalingGroup.MinSize) + summary.MaxSize = int(*scalingGroup.MaxSize) + + if summary.DesiredCapacity > 0 { + summary.Version, err = kubewrapper.GetNodegroupKubernetesVersion(m.clientSet.CoreV1().Nodes(), summary.Name) + if err != nil { + return nil, fmt.Errorf("getting nodegroup's kubernetes version: %w", err) + } + } + + return summary, nil +} + +func getNodeGroupPaths(tags []*cfn.Tag) (*nodeGroupPaths, error) { + nodeGroupType, err := manager.GetNodeGroupType(tags) if err != nil { - return nil, errors.Wrap(err, "getting nodegroup stack summaries") + return nil, err } - if len(summaries) > 0 { - s := summaries[0] - if s.DesiredCapacity > 0 { - s.Version, err = kubewrapper.GetNodegroupKubernetesVersion(m.clientSet.CoreV1().Nodes(), s.Name) - if err != nil { - return nil, errors.Wrap(err, "getting nodegroup's kubernetes version") - } + switch nodeGroupType { + case api.NodeGroupTypeManaged: + makePath := func(fieldPath string) string { + return fmt.Sprintf("%s.ManagedNodeGroup.Properties.%s", resourcesRootPath, fieldPath) } - return s, nil + makeScalingPath := func(field string) string { + return makePath(fmt.Sprintf("ScalingConfig.%s", field)) + } + return &nodeGroupPaths{ + InstanceType: makePath("InstanceTypes.0"), + DesiredCapacity: makeScalingPath("DesiredSize"), + MinSize: makeScalingPath("MinSize"), + MaxSize: makeScalingPath("MaxSize"), + }, nil + + // Tag may not exist for existing nodegroups + case api.NodeGroupTypeUnmanaged, "": + makePath := func(field string) string { + return fmt.Sprintf("%s.NodeGroup.Properties.%s", resourcesRootPath, field) + } + return &nodeGroupPaths{ + InstanceType: resourcesRootPath + ".NodeGroupLaunchTemplate.Properties.LaunchTemplateData.InstanceType", + DesiredCapacity: makePath("DesiredCapacity"), + MinSize: makePath("MinSize"), + MaxSize: makePath("MaxSize"), + }, nil + + default: + return nil, fmt.Errorf("unexpected nodegroup type tag: %q", nodeGroupType) } - return m.makeManagedNGSummary(name) } -func (m *Manager) makeManagedNGSummary(nodeGroupName string) (*manager.NodeGroupSummary, error) { +type nodeGroupPaths struct { + InstanceType string + DesiredCapacity string + MinSize string + MaxSize string +} + +func (m *Manager) mapStackToNodeGroupSummary(stack *manager.Stack, ngPaths *nodeGroupPaths) (*Summary, error) { + template, err := m.stackManager.GetStackTemplate(*stack.StackName) + if err != nil { + return nil, fmt.Errorf("error getting CloudFormation template for stack %s: %w", *stack.StackName, err) + } + + summary := &Summary{ + StackName: *stack.StackName, + Cluster: getClusterNameTag(stack), + Name: m.stackManager.GetNodeGroupName(stack), + Status: *stack.StackStatus, + MaxSize: int(gjson.Get(template, ngPaths.MaxSize).Int()), + MinSize: int(gjson.Get(template, ngPaths.MinSize).Int()), + DesiredCapacity: int(gjson.Get(template, ngPaths.DesiredCapacity).Int()), + InstanceType: gjson.Get(template, ngPaths.InstanceType).String(), + ImageID: gjson.Get(template, imageIDPath).String(), + CreationTime: *stack.CreationTime, + } + + nodeGroupType, err := manager.GetNodeGroupType(stack.Tags) + if err != nil { + return nil, err + } + + var nodeInstanceRoleARN string + if nodeGroupType == api.NodeGroupTypeUnmanaged { + nodeInstanceRoleARNCollector := func(s string) error { + nodeInstanceRoleARN = s + return nil + } + collectors := map[string]outputs.Collector{ + outputs.NodeGroupInstanceRoleARN: nodeInstanceRoleARNCollector, + } + collectorSet := outputs.NewCollectorSet(collectors) + if err := collectorSet.MustCollect(*stack); err != nil { + logger.Warning(fmt.Errorf("error collecting Cloudformation outputs for stack %s: %w", *stack.StackName, err).Error()) + } + } + + summary.NodeInstanceRoleARN = nodeInstanceRoleARN + + return summary, nil +} + +func getClusterNameTag(s *manager.Stack) string { + for _, tag := range s.Tags { + if *tag.Key == api.ClusterNameTag || *tag.Key == api.OldClusterNameTag { + return *tag.Value + } + } + return "" +} + +func (m *Manager) getManagedSummary(nodeGroupName string) (*Summary, error) { describeOutput, err := m.ctl.Provider.EKS().DescribeNodegroup(&eks.DescribeNodegroupInput{ ClusterName: aws.String(m.cfg.Metadata.Name), NodegroupName: aws.String(nodeGroupName), @@ -105,7 +302,7 @@ func (m *Manager) makeManagedNGSummary(nodeGroupName string) (*manager.NodeGroup imageID = *ng.AmiType } - return &manager.NodeGroupSummary{ + return &Summary{ Name: *ng.NodegroupName, Cluster: *ng.ClusterName, Status: *ng.Status, diff --git a/pkg/actions/nodegroup/get_test.go b/pkg/actions/nodegroup/get_test.go index 96416c2bed..28cfc473a6 100644 --- a/pkg/actions/nodegroup/get_test.go +++ b/pkg/actions/nodegroup/get_test.go @@ -1,10 +1,12 @@ package nodegroup_test import ( + "context" "fmt" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/ec2" awseks "github.com/aws/aws-sdk-go/service/eks" @@ -15,33 +17,32 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/nodegroup" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/cfn/manager/fakes" "github.com/weaveworks/eksctl/pkg/eks" "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("Get", func() { var ( - clusterName, stackName, ngName string - t time.Time - p *mockprovider.MockProvider - cfg *api.ClusterConfig - m *nodegroup.Manager - fakeStackManager *fakes.FakeStackManager - fakeClientSet *fake.Clientset + ngName = "my-nodegroup" + stackName = "stack-name" + clusterName = "my-cluster" + t = time.Now() + p *mockprovider.MockProvider + cfg *api.ClusterConfig + m *nodegroup.Manager + fakeStackManager *fakes.FakeStackManager + fakeClientSet *fake.Clientset ) BeforeEach(func() { - t = time.Now() - ngName = "my-nodegroup" - clusterName = "my-cluster" cfg = api.NewClusterConfig() cfg.Metadata.Name = clusterName p = mockprovider.NewMockProvider() fakeClientSet = fake.NewSimpleClientset() m = nodegroup.New(cfg, &eks.ClusterProvider{Provider: p}, fakeClientSet) - fakeStackManager = new(fakes.FakeStackManager) m.SetStackManager(fakeStackManager) }) @@ -110,8 +111,8 @@ var _ = Describe("Get", func() { Expect(summaries).To(HaveLen(1)) ngSummary := *summaries[0] - Expect(ngSummary).To(Equal(manager.NodeGroupSummary{ - StackName: "", + Expect(ngSummary).To(Equal(nodegroup.Summary{ + StackName: stackName, Cluster: clusterName, Name: ngName, Status: "my-status", @@ -174,7 +175,7 @@ var _ = Describe("Get", func() { Expect(summaries).To(HaveLen(1)) ngSummary := *summaries[0] - Expect(ngSummary).To(Equal(manager.NodeGroupSummary{ + Expect(ngSummary).To(Equal(nodegroup.Summary{ StackName: "", Cluster: clusterName, Name: ngName, @@ -193,7 +194,7 @@ var _ = Describe("Get", func() { }) }) - When("a nodegroup has multiple ASGs", func() { + When("a nodegroup is associated with a launch template", func() { BeforeEach(func() { p.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ ClusterName: aws.String(clusterName), @@ -214,30 +215,46 @@ var _ = Describe("Get", func() { MaxSize: aws.Int64(4), MinSize: aws.Int64(0), }, - InstanceTypes: aws.StringSlice([]string{"m5.xlarge"}), + InstanceTypes: []*string{}, AmiType: aws.String("ami-type"), CreatedAt: &t, NodeRole: aws.String("node-role"), Resources: &awseks.NodegroupResources{ AutoScalingGroups: []*awseks.AutoScalingGroup{ { - Name: aws.String("asg-1"), - }, - { - Name: aws.String("asg-2"), + Name: aws.String("asg-name"), }, }, }, Version: aws.String("1.18"), + LaunchTemplate: &awseks.LaunchTemplateSpecification{ + Id: aws.String("4"), + Version: aws.String("5"), + }, }, }, nil) + + p.MockEC2().On("DescribeLaunchTemplateVersions", &ec2.DescribeLaunchTemplateVersionsInput{ + LaunchTemplateId: aws.String("4"), + }).Return(&ec2.DescribeLaunchTemplateVersionsOutput{LaunchTemplateVersions: []*ec2.LaunchTemplateVersion{ + { + LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ + InstanceType: aws.String("big"), + }, + VersionNumber: aws.Int64(5), + }, + }}, nil) }) - It("returns all ASG names in the summary", func() { - summary, err := m.Get(ngName) + It("returns a summary of the node group with the instance type from the launch template", func() { + fakeStackManager.DescribeNodeGroupStackReturns(nil, fmt.Errorf("error describing cloudformation stack")) + + summaries, err := m.GetAll() Expect(err).NotTo(HaveOccurred()) + Expect(summaries).To(HaveLen(1)) - Expect(*summary).To(Equal(manager.NodeGroupSummary{ + ngSummary := *summaries[0] + Expect(ngSummary).To(Equal(nodegroup.Summary{ StackName: "", Cluster: clusterName, Name: ngName, @@ -245,18 +262,18 @@ var _ = Describe("Get", func() { MaxSize: 4, MinSize: 0, DesiredCapacity: 2, - InstanceType: "m5.xlarge", + InstanceType: "big", ImageID: "ami-type", CreationTime: t, NodeInstanceRoleARN: "node-role", - AutoScalingGroupName: "asg-1,asg-2", + AutoScalingGroupName: "asg-name", Version: "1.18", NodeGroupType: api.NodeGroupTypeManaged, })) }) }) - When("a nodegroup is associated with a launch template", func() { + When("a nodegroup has a custom AMI", func() { BeforeEach(func() { p.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ ClusterName: aws.String(clusterName), @@ -277,58 +294,49 @@ var _ = Describe("Get", func() { MaxSize: aws.Int64(4), MinSize: aws.Int64(0), }, - InstanceTypes: []*string{}, - AmiType: aws.String("ami-type"), - CreatedAt: &t, - NodeRole: aws.String("node-role"), + InstanceTypes: aws.StringSlice([]string{"m5.xlarge"}), + AmiType: aws.String("CUSTOM"), + CreatedAt: &t, + NodeRole: aws.String("node-role"), + ReleaseVersion: aws.String("ami-custom"), Resources: &awseks.NodegroupResources{ AutoScalingGroups: []*awseks.AutoScalingGroup{ { - Name: aws.String("asg-name"), + Name: aws.String("asg-1"), + }, + { + Name: aws.String("asg-2"), }, }, }, Version: aws.String("1.18"), - LaunchTemplate: &awseks.LaunchTemplateSpecification{ - Id: aws.String("4"), - Version: aws.String("5"), - }, }, }, nil) - p.MockEC2().On("DescribeLaunchTemplateVersions", &ec2.DescribeLaunchTemplateVersionsInput{ - LaunchTemplateId: aws.String("4"), - }).Return(&ec2.DescribeLaunchTemplateVersionsOutput{LaunchTemplateVersions: []*ec2.LaunchTemplateVersion{ - { - LaunchTemplateData: &ec2.ResponseLaunchTemplateData{ - InstanceType: aws.String("big"), - }, - VersionNumber: aws.Int64(5), - }, - }}, nil) + fakeStackManager.DescribeNodeGroupStackReturns(&cloudformation.Stack{ + StackName: aws.String(stackName), + }, nil) }) - It("returns a summary of the node group with the instance type from the launch template", func() { - fakeStackManager.DescribeNodeGroupStackReturns(nil, fmt.Errorf("error describing cloudformation stack")) - + It("returns the AMI ID instead of `CUSTOM`", func() { summaries, err := m.GetAll() Expect(err).NotTo(HaveOccurred()) Expect(summaries).To(HaveLen(1)) ngSummary := *summaries[0] - Expect(ngSummary).To(Equal(manager.NodeGroupSummary{ - StackName: "", + Expect(ngSummary).To(Equal(nodegroup.Summary{ + StackName: stackName, Cluster: clusterName, Name: ngName, Status: "my-status", MaxSize: 4, MinSize: 0, DesiredCapacity: 2, - InstanceType: "big", - ImageID: "ami-type", + InstanceType: "m5.xlarge", + ImageID: "ami-custom", CreationTime: t, NodeInstanceRoleARN: "node-role", - AutoScalingGroupName: "asg-name", + AutoScalingGroupName: "asg-1,asg-2", Version: "1.18", NodeGroupType: api.NodeGroupTypeManaged, })) @@ -336,8 +344,70 @@ var _ = Describe("Get", func() { }) }) - When("a nodegroup has a custom AMI", func() { + Context("when getting unmanaged nodegroups", func() { + var ( + creationTime = time.Now() + unmanagedStackName = "unmanaged-stack" + unmanagedNodegroupName = "unmanaged-ng" + unmanagedTemplate = ` +{ + "Resources": { + "NodeGroup": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "DesiredCapacity": "3", + "MaxSize": "6", + "MinSize": "1" + } + } + } +} + +` + ) + BeforeEach(func() { + //unmanaged nodegroup + fakeStackManager.DescribeNodeGroupStacksReturns([]*cloudformation.Stack{ + { + StackName: aws.String(unmanagedStackName), + Tags: []*cloudformation.Tag{ + { + Key: aws.String(api.NodeGroupNameTag), + Value: aws.String(unmanagedNodegroupName), + }, + { + Key: aws.String(api.ClusterNameTag), + Value: aws.String(clusterName), + }, + }, + StackStatus: aws.String("CREATE_COMPLETE"), + CreationTime: aws.Time(creationTime), + }, + }, nil) + fakeStackManager.GetStackTemplateReturns(unmanagedTemplate, nil) + fakeStackManager.GetUnmanagedNodeGroupAutoScalingGroupNameReturns("asg", nil) + fakeStackManager.GetAutoScalingGroupDesiredCapacityReturns(autoscaling.Group{ + DesiredCapacity: aws.Int64(50), + MinSize: aws.Int64(1), + MaxSize: aws.Int64(100), + }, nil) + + _, _ = fakeClientSet.CoreV1().Nodes().Create(context.TODO(), &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "alpha.eksctl.io/nodegroup-name": unmanagedNodegroupName, + }, + }, + Status: corev1.NodeStatus{ + NodeInfo: corev1.NodeSystemInfo{ + KubeletVersion: "1.21.1", + }, + }, + }, metav1.CreateOptions{}) + fakeStackManager.GetNodeGroupNameReturns(unmanagedNodegroupName) + + //managed nodegroup p.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ ClusterName: aws.String(clusterName), NodegroupName: aws.String(ngName), @@ -357,18 +427,14 @@ var _ = Describe("Get", func() { MaxSize: aws.Int64(4), MinSize: aws.Int64(0), }, - InstanceTypes: aws.StringSlice([]string{"m5.xlarge"}), - AmiType: aws.String("CUSTOM"), - CreatedAt: &t, - NodeRole: aws.String("node-role"), - ReleaseVersion: aws.String("ami-custom"), + InstanceTypes: []*string{}, + AmiType: aws.String("ami-type"), + CreatedAt: &t, + NodeRole: aws.String("node-role"), Resources: &awseks.NodegroupResources{ AutoScalingGroups: []*awseks.AutoScalingGroup{ { - Name: aws.String("asg-1"), - }, - { - Name: aws.String("asg-2"), + Name: aws.String("asg-name"), }, }, }, @@ -381,98 +447,124 @@ var _ = Describe("Get", func() { }, nil) }) - It("returns the AMI ID instead of `CUSTOM`", func() { + It("returns the nodegroups with the kubernetes version", func() { summaries, err := m.GetAll() Expect(err).NotTo(HaveOccurred()) - Expect(summaries).To(HaveLen(1)) + Expect(summaries).To(HaveLen(2)) + + unmanagedSummary := *summaries[0] + Expect(unmanagedSummary).To(Equal(nodegroup.Summary{ + StackName: unmanagedStackName, + Cluster: clusterName, + Name: unmanagedNodegroupName, + Status: "CREATE_COMPLETE", + AutoScalingGroupName: "asg", + MaxSize: 100, + DesiredCapacity: 50, + MinSize: 1, + Version: "1.21.1", + CreationTime: creationTime, + NodeGroupType: api.NodeGroupTypeUnmanaged, + })) - ngSummary := *summaries[0] - Expect(ngSummary).To(Equal(manager.NodeGroupSummary{ - StackName: "", + Expect(*summaries[1]).To(Equal(nodegroup.Summary{ + StackName: stackName, Cluster: clusterName, Name: ngName, Status: "my-status", MaxSize: 4, MinSize: 0, DesiredCapacity: 2, - InstanceType: "m5.xlarge", - ImageID: "ami-custom", + InstanceType: "-", + ImageID: "ami-type", CreationTime: t, NodeInstanceRoleARN: "node-role", - AutoScalingGroupName: "asg-1,asg-2", + AutoScalingGroupName: "asg-name", Version: "1.18", NodeGroupType: api.NodeGroupTypeManaged, })) }) }) + }) - Context("when getting unmanaged nodegroups", func() { - When("the DesiredCapacity is 0", func() { - BeforeEach(func() { - p.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ - ClusterName: aws.String(clusterName), - NodegroupName: aws.String(ngName), - }).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(1)) - Expect(args[0]).To(BeAssignableToTypeOf(&awseks.DescribeNodegroupInput{ - ClusterName: aws.String(clusterName), - NodegroupName: aws.String(ngName), - })) - }).Return(&awseks.DescribeNodegroupOutput{ - Nodegroup: &awseks.Nodegroup{ - NodegroupName: aws.String(ngName), - ClusterName: aws.String(clusterName), - Status: aws.String("my-status"), - ScalingConfig: &awseks.NodegroupScalingConfig{ - DesiredSize: aws.Int64(2), - MaxSize: aws.Int64(4), - MinSize: aws.Int64(0), + Describe("Get", func() { + BeforeEach(func() { + fakeStackManager.DescribeNodeGroupStackReturns(&cloudformation.Stack{ + StackName: aws.String(stackName), + Tags: []*cloudformation.Tag{ + { + Key: aws.String(api.NodeGroupNameTag), + Value: aws.String(ngName), + }, + { + Key: aws.String(api.ClusterNameTag), + Value: aws.String(clusterName), + }, + { + Key: aws.String(api.NodeGroupTypeTag), + Value: aws.String(string(api.NodeGroupTypeManaged)), + }, + }, + }, nil) + + p.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(ngName), + }).Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(1)) + Expect(args[0]).To(BeAssignableToTypeOf(&awseks.DescribeNodegroupInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(ngName), + })) + }).Return(&awseks.DescribeNodegroupOutput{ + Nodegroup: &awseks.Nodegroup{ + NodegroupName: aws.String(ngName), + ClusterName: aws.String(clusterName), + Status: aws.String("my-status"), + ScalingConfig: &awseks.NodegroupScalingConfig{ + DesiredSize: aws.Int64(2), + MaxSize: aws.Int64(4), + MinSize: aws.Int64(0), + }, + InstanceTypes: aws.StringSlice([]string{"m5.xlarge"}), + AmiType: aws.String("ami-type"), + CreatedAt: &t, + NodeRole: aws.String("node-role"), + Resources: &awseks.NodegroupResources{ + AutoScalingGroups: []*awseks.AutoScalingGroup{ + { + Name: aws.String("asg-1"), }, - InstanceTypes: []*string{}, - AmiType: aws.String("ami-type"), - CreatedAt: &t, - NodeRole: aws.String("node-role"), - Resources: &awseks.NodegroupResources{ - AutoScalingGroups: []*awseks.AutoScalingGroup{ - { - Name: aws.String("asg-name"), - }, - }, + { + Name: aws.String("asg-2"), }, - Version: aws.String("1.18"), }, - }, nil) - - fakeStackManager.DescribeNodeGroupStackReturns(&cloudformation.Stack{ - StackName: aws.String(stackName), - }, nil) - }) - - It("does not return the k8s version of the nodes", func() { - fakeStackManager.GetUnmanagedNodeGroupSummariesReturns([]*manager.NodeGroupSummary{ - { - DesiredCapacity: 0, - Cluster: clusterName, - Name: ngName, - NodeGroupType: api.NodeGroupTypeUnmanaged, - }, - }, nil) + }, + Version: aws.String("1.18"), + }, + }, nil) + }) - summaries, err := m.GetAll() - Expect(err).NotTo(HaveOccurred()) - Expect(summaries).To(HaveLen(2)) - - unmanagedSummary := *summaries[0] - Expect(unmanagedSummary).To(Equal(manager.NodeGroupSummary{ - StackName: "", - Cluster: clusterName, - Name: ngName, - DesiredCapacity: 0, - Version: "", - NodeGroupType: api.NodeGroupTypeUnmanaged, - })) - }) - }) + It("returns the summary", func() { + summary, err := m.Get(ngName) + Expect(err).NotTo(HaveOccurred()) + + Expect(*summary).To(Equal(nodegroup.Summary{ + StackName: "", + Cluster: clusterName, + Name: ngName, + Status: "my-status", + MaxSize: 4, + MinSize: 0, + DesiredCapacity: 2, + InstanceType: "m5.xlarge", + ImageID: "ami-type", + CreationTime: t, + NodeInstanceRoleARN: "node-role", + AutoScalingGroupName: "asg-1,asg-2", + Version: "1.18", + NodeGroupType: api.NodeGroupTypeManaged, + })) }) }) }) diff --git a/pkg/cfn/manager/fakes/fake_stack_manager.go b/pkg/cfn/manager/fakes/fake_stack_manager.go index f47753d8f2..2a3cc95861 100644 --- a/pkg/cfn/manager/fakes/fake_stack_manager.go +++ b/pkg/cfn/manager/fakes/fake_stack_manager.go @@ -4,6 +4,7 @@ package fakes import ( "sync" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudtrail" "github.com/aws/aws-sdk-go/service/eks/eksiface" @@ -254,6 +255,19 @@ type FakeStackManager struct { fixClusterCompatibilityReturnsOnCall map[int]struct { result1 error } + GetAutoScalingGroupDesiredCapacityStub func(string) (autoscaling.Group, error) + getAutoScalingGroupDesiredCapacityMutex sync.RWMutex + getAutoScalingGroupDesiredCapacityArgsForCall []struct { + arg1 string + } + getAutoScalingGroupDesiredCapacityReturns struct { + result1 autoscaling.Group + result2 error + } + getAutoScalingGroupDesiredCapacityReturnsOnCall map[int]struct { + result1 autoscaling.Group + result2 error + } GetAutoScalingGroupNameStub func(*cloudformation.Stack) (string, error) getAutoScalingGroupNameMutex sync.RWMutex getAutoScalingGroupNameArgsForCall []struct { @@ -388,17 +402,17 @@ type FakeStackManager struct { result1 string result2 error } - GetUnmanagedNodeGroupSummariesStub func(string) ([]*manager.NodeGroupSummary, error) - getUnmanagedNodeGroupSummariesMutex sync.RWMutex - getUnmanagedNodeGroupSummariesArgsForCall []struct { - arg1 string + GetUnmanagedNodeGroupAutoScalingGroupNameStub func(*cloudformation.Stack) (string, error) + getUnmanagedNodeGroupAutoScalingGroupNameMutex sync.RWMutex + getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall []struct { + arg1 *cloudformation.Stack } - getUnmanagedNodeGroupSummariesReturns struct { - result1 []*manager.NodeGroupSummary + getUnmanagedNodeGroupAutoScalingGroupNameReturns struct { + result1 string result2 error } - getUnmanagedNodeGroupSummariesReturnsOnCall map[int]struct { - result1 []*manager.NodeGroupSummary + getUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall map[int]struct { + result1 string result2 error } HasClusterStackUsingCachedListStub func([]string, string) (bool, error) @@ -1868,6 +1882,70 @@ func (fake *FakeStackManager) FixClusterCompatibilityReturnsOnCall(i int, result }{result1} } +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacity(arg1 string) (autoscaling.Group, error) { + fake.getAutoScalingGroupDesiredCapacityMutex.Lock() + ret, specificReturn := fake.getAutoScalingGroupDesiredCapacityReturnsOnCall[len(fake.getAutoScalingGroupDesiredCapacityArgsForCall)] + fake.getAutoScalingGroupDesiredCapacityArgsForCall = append(fake.getAutoScalingGroupDesiredCapacityArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetAutoScalingGroupDesiredCapacityStub + fakeReturns := fake.getAutoScalingGroupDesiredCapacityReturns + fake.recordInvocation("GetAutoScalingGroupDesiredCapacity", []interface{}{arg1}) + fake.getAutoScalingGroupDesiredCapacityMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacityCallCount() int { + fake.getAutoScalingGroupDesiredCapacityMutex.RLock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.RUnlock() + return len(fake.getAutoScalingGroupDesiredCapacityArgsForCall) +} + +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacityCalls(stub func(string) (autoscaling.Group, error)) { + fake.getAutoScalingGroupDesiredCapacityMutex.Lock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.Unlock() + fake.GetAutoScalingGroupDesiredCapacityStub = stub +} + +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacityArgsForCall(i int) string { + fake.getAutoScalingGroupDesiredCapacityMutex.RLock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.RUnlock() + argsForCall := fake.getAutoScalingGroupDesiredCapacityArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacityReturns(result1 autoscaling.Group, result2 error) { + fake.getAutoScalingGroupDesiredCapacityMutex.Lock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.Unlock() + fake.GetAutoScalingGroupDesiredCapacityStub = nil + fake.getAutoScalingGroupDesiredCapacityReturns = struct { + result1 autoscaling.Group + result2 error + }{result1, result2} +} + +func (fake *FakeStackManager) GetAutoScalingGroupDesiredCapacityReturnsOnCall(i int, result1 autoscaling.Group, result2 error) { + fake.getAutoScalingGroupDesiredCapacityMutex.Lock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.Unlock() + fake.GetAutoScalingGroupDesiredCapacityStub = nil + if fake.getAutoScalingGroupDesiredCapacityReturnsOnCall == nil { + fake.getAutoScalingGroupDesiredCapacityReturnsOnCall = make(map[int]struct { + result1 autoscaling.Group + result2 error + }) + } + fake.getAutoScalingGroupDesiredCapacityReturnsOnCall[i] = struct { + result1 autoscaling.Group + result2 error + }{result1, result2} +} + func (fake *FakeStackManager) GetAutoScalingGroupName(arg1 *cloudformation.Stack) (string, error) { fake.getAutoScalingGroupNameMutex.Lock() ret, specificReturn := fake.getAutoScalingGroupNameReturnsOnCall[len(fake.getAutoScalingGroupNameArgsForCall)] @@ -2526,16 +2604,16 @@ func (fake *FakeStackManager) GetStackTemplateReturnsOnCall(i int, result1 strin }{result1, result2} } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummaries(arg1 string) ([]*manager.NodeGroupSummary, error) { - fake.getUnmanagedNodeGroupSummariesMutex.Lock() - ret, specificReturn := fake.getUnmanagedNodeGroupSummariesReturnsOnCall[len(fake.getUnmanagedNodeGroupSummariesArgsForCall)] - fake.getUnmanagedNodeGroupSummariesArgsForCall = append(fake.getUnmanagedNodeGroupSummariesArgsForCall, struct { - arg1 string +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupName(arg1 *cloudformation.Stack) (string, error) { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Lock() + ret, specificReturn := fake.getUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall[len(fake.getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall)] + fake.getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall = append(fake.getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall, struct { + arg1 *cloudformation.Stack }{arg1}) - stub := fake.GetUnmanagedNodeGroupSummariesStub - fakeReturns := fake.getUnmanagedNodeGroupSummariesReturns - fake.recordInvocation("GetUnmanagedNodeGroupSummaries", []interface{}{arg1}) - fake.getUnmanagedNodeGroupSummariesMutex.Unlock() + stub := fake.GetUnmanagedNodeGroupAutoScalingGroupNameStub + fakeReturns := fake.getUnmanagedNodeGroupAutoScalingGroupNameReturns + fake.recordInvocation("GetUnmanagedNodeGroupAutoScalingGroupName", []interface{}{arg1}) + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Unlock() if stub != nil { return stub(arg1) } @@ -2545,47 +2623,47 @@ func (fake *FakeStackManager) GetUnmanagedNodeGroupSummaries(arg1 string) ([]*ma return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummariesCallCount() int { - fake.getUnmanagedNodeGroupSummariesMutex.RLock() - defer fake.getUnmanagedNodeGroupSummariesMutex.RUnlock() - return len(fake.getUnmanagedNodeGroupSummariesArgsForCall) +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupNameCallCount() int { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RLock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RUnlock() + return len(fake.getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall) } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummariesCalls(stub func(string) ([]*manager.NodeGroupSummary, error)) { - fake.getUnmanagedNodeGroupSummariesMutex.Lock() - defer fake.getUnmanagedNodeGroupSummariesMutex.Unlock() - fake.GetUnmanagedNodeGroupSummariesStub = stub +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupNameCalls(stub func(*cloudformation.Stack) (string, error)) { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Lock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Unlock() + fake.GetUnmanagedNodeGroupAutoScalingGroupNameStub = stub } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummariesArgsForCall(i int) string { - fake.getUnmanagedNodeGroupSummariesMutex.RLock() - defer fake.getUnmanagedNodeGroupSummariesMutex.RUnlock() - argsForCall := fake.getUnmanagedNodeGroupSummariesArgsForCall[i] +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupNameArgsForCall(i int) *cloudformation.Stack { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RLock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RUnlock() + argsForCall := fake.getUnmanagedNodeGroupAutoScalingGroupNameArgsForCall[i] return argsForCall.arg1 } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummariesReturns(result1 []*manager.NodeGroupSummary, result2 error) { - fake.getUnmanagedNodeGroupSummariesMutex.Lock() - defer fake.getUnmanagedNodeGroupSummariesMutex.Unlock() - fake.GetUnmanagedNodeGroupSummariesStub = nil - fake.getUnmanagedNodeGroupSummariesReturns = struct { - result1 []*manager.NodeGroupSummary +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupNameReturns(result1 string, result2 error) { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Lock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Unlock() + fake.GetUnmanagedNodeGroupAutoScalingGroupNameStub = nil + fake.getUnmanagedNodeGroupAutoScalingGroupNameReturns = struct { + result1 string result2 error }{result1, result2} } -func (fake *FakeStackManager) GetUnmanagedNodeGroupSummariesReturnsOnCall(i int, result1 []*manager.NodeGroupSummary, result2 error) { - fake.getUnmanagedNodeGroupSummariesMutex.Lock() - defer fake.getUnmanagedNodeGroupSummariesMutex.Unlock() - fake.GetUnmanagedNodeGroupSummariesStub = nil - if fake.getUnmanagedNodeGroupSummariesReturnsOnCall == nil { - fake.getUnmanagedNodeGroupSummariesReturnsOnCall = make(map[int]struct { - result1 []*manager.NodeGroupSummary +func (fake *FakeStackManager) GetUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall(i int, result1 string, result2 error) { + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Lock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.Unlock() + fake.GetUnmanagedNodeGroupAutoScalingGroupNameStub = nil + if fake.getUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall == nil { + fake.getUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall = make(map[int]struct { + result1 string result2 error }) } - fake.getUnmanagedNodeGroupSummariesReturnsOnCall[i] = struct { - result1 []*manager.NodeGroupSummary + fake.getUnmanagedNodeGroupAutoScalingGroupNameReturnsOnCall[i] = struct { + result1 string result2 error }{result1, result2} } @@ -4216,6 +4294,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.ensureMapPublicIPOnLaunchEnabledMutex.RUnlock() fake.fixClusterCompatibilityMutex.RLock() defer fake.fixClusterCompatibilityMutex.RUnlock() + fake.getAutoScalingGroupDesiredCapacityMutex.RLock() + defer fake.getAutoScalingGroupDesiredCapacityMutex.RUnlock() fake.getAutoScalingGroupNameMutex.RLock() defer fake.getAutoScalingGroupNameMutex.RUnlock() fake.getClusterStackIfExistsMutex.RLock() @@ -4238,8 +4318,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.getNodeGroupStackTypeMutex.RUnlock() fake.getStackTemplateMutex.RLock() defer fake.getStackTemplateMutex.RUnlock() - fake.getUnmanagedNodeGroupSummariesMutex.RLock() - defer fake.getUnmanagedNodeGroupSummariesMutex.RUnlock() + fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RLock() + defer fake.getUnmanagedNodeGroupAutoScalingGroupNameMutex.RUnlock() fake.hasClusterStackUsingCachedListMutex.RLock() defer fake.hasClusterStackUsingCachedListMutex.RUnlock() fake.listClusterStackNamesMutex.RLock() diff --git a/pkg/cfn/manager/interface.go b/pkg/cfn/manager/interface.go index 2addda5ef3..52f61dd832 100644 --- a/pkg/cfn/manager/interface.go +++ b/pkg/cfn/manager/interface.go @@ -1,6 +1,7 @@ package manager import ( + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudtrail" "github.com/aws/aws-sdk-go/service/eks/eksiface" @@ -30,6 +31,8 @@ type GetNodegroupOption struct { NodeGroupName string } +var _ StackManager = &StackCollection{} + //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate //counterfeiter:generate -o fakes/fake_stack_manager.go . StackManager type StackManager interface { @@ -52,6 +55,7 @@ type StackManager interface { DoWaitUntilStackIsCreated(i *Stack) error EnsureMapPublicIPOnLaunchEnabled() error FixClusterCompatibility() error + GetAutoScalingGroupDesiredCapacity(name string) (autoscaling.Group, error) GetAutoScalingGroupName(s *Stack) (string, error) GetClusterStackIfExists() (*Stack, error) GetFargateStack() (*Stack, error) @@ -63,7 +67,7 @@ type StackManager interface { GetNodeGroupName(s *Stack) string GetNodeGroupStackType(options GetNodegroupOption) (v1alpha5.NodeGroupType, error) GetStackTemplate(stackName string) (string, error) - GetUnmanagedNodeGroupSummaries(name string) ([]*NodeGroupSummary, error) + GetUnmanagedNodeGroupAutoScalingGroupName(s *Stack) (string, error) HasClusterStackUsingCachedList(clusterStackNames []string, clusterName string) (bool, error) ListClusterStackNames() ([]string, error) ListIAMServiceAccountStacks() ([]string, error) diff --git a/pkg/cfn/manager/nodegroup.go b/pkg/cfn/manager/nodegroup.go index 5476bb4d94..fd9b6daf28 100644 --- a/pkg/cfn/manager/nodegroup.go +++ b/pkg/cfn/manager/nodegroup.go @@ -3,7 +3,6 @@ package manager import ( "fmt" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/autoscaling" @@ -12,38 +11,14 @@ import ( "github.com/blang/semver" "github.com/kris-nova/logger" "github.com/pkg/errors" - "github.com/tidwall/gjson" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/builder" - "github.com/weaveworks/eksctl/pkg/cfn/outputs" "github.com/weaveworks/eksctl/pkg/nodebootstrap" "github.com/weaveworks/eksctl/pkg/version" "github.com/weaveworks/eksctl/pkg/vpc" ) -const ( - imageIDPath = resourcesRootPath + ".NodeGroupLaunchTemplate.Properties.LaunchTemplateData.ImageId" -) - -// NodeGroupSummary represents a summary of a nodegroup stack -type NodeGroupSummary struct { - StackName string - Cluster string - Name string - Status string - MaxSize int - MinSize int - DesiredCapacity int - InstanceType string - ImageID string - CreationTime time.Time - NodeInstanceRoleARN string - AutoScalingGroupName string - Version string - NodeGroupType api.NodeGroupType `json:"Type"` -} - // NodeGroupStack represents a nodegroup and its type type NodeGroupStack struct { NodeGroupName string @@ -175,60 +150,6 @@ func (c *StackCollection) DescribeNodeGroupStacksAndResources() (map[string]Stac return allResources, nil } -// GetUnmanagedNodeGroupSummaries returns a list of summaries for the unmanaged nodegroups of a cluster -func (c *StackCollection) GetUnmanagedNodeGroupSummaries(name string) ([]*NodeGroupSummary, error) { - stacks, err := c.DescribeNodeGroupStacks() - if err != nil { - return nil, errors.Wrap(err, "getting nodegroup stacks") - } - - // Create an empty array here so that an object is returned rather than null - summaries := []*NodeGroupSummary{} - for _, s := range stacks { - nodeGroupType, err := GetNodeGroupType(s.Tags) - if err != nil { - return nil, err - } - - if nodeGroupType != api.NodeGroupTypeUnmanaged { - continue - } - - ngPaths, err := getNodeGroupPaths(s.Tags) - if err != nil { - return nil, err - } - - summary, err := c.mapStackToNodeGroupSummary(s, ngPaths) - - if err != nil { - return nil, errors.Wrap(err, "mapping stack to nodegroup summary") - } - summary.NodeGroupType = api.NodeGroupTypeUnmanaged - - asgName, err := c.getUnmanagedNodeGroupAutoScalingGroupName(s) - if err != nil { - return nil, errors.Wrap(err, "getting autoscalinggroupname") - } - - summary.AutoScalingGroupName = asgName - - scalingGroup, err := c.GetAutoScalingGroupDesiredCapacity(asgName) - if err != nil { - return nil, errors.Wrap(err, "getting autoscalinggroup desired capacity") - } - summary.DesiredCapacity = int(*scalingGroup.DesiredCapacity) - summary.MinSize = int(*scalingGroup.MinSize) - summary.MaxSize = int(*scalingGroup.MaxSize) - - if name == "" || summary.Name == name { - summaries = append(summaries, summary) - } - } - - return summaries, nil -} - func (c *StackCollection) GetAutoScalingGroupName(s *Stack) (string, error) { nodeGroupType, err := GetNodeGroupType(s.Tags) @@ -244,7 +165,7 @@ func (c *StackCollection) GetAutoScalingGroupName(s *Stack) (string, error) { } return res, nil case api.NodeGroupTypeUnmanaged, "": - res, err := c.getUnmanagedNodeGroupAutoScalingGroupName(s) + res, err := c.GetUnmanagedNodeGroupAutoScalingGroupName(s) if err != nil { return "", err } @@ -256,7 +177,7 @@ func (c *StackCollection) GetAutoScalingGroupName(s *Stack) (string, error) { } // GetNodeGroupAutoScalingGroupName returns the unmanaged nodegroup's AutoScalingGroupName -func (c *StackCollection) getUnmanagedNodeGroupAutoScalingGroupName(s *Stack) (string, error) { +func (c *StackCollection) GetUnmanagedNodeGroupAutoScalingGroupName(s *Stack) (string, error) { input := &cfn.DescribeStackResourceInput{ StackName: s.StackName, LogicalResourceId: aws.String("NodeGroup"), @@ -370,96 +291,6 @@ func GetEksctlVersionFromTags(tags []*cfn.Tag) (semver.Version, bool, error) { return semver.Version{}, false, nil } -type nodeGroupPaths struct { - InstanceType string - DesiredCapacity string - MinSize string - MaxSize string -} - -func getNodeGroupPaths(tags []*cfn.Tag) (*nodeGroupPaths, error) { - nodeGroupType, err := GetNodeGroupType(tags) - if err != nil { - return nil, err - } - - switch nodeGroupType { - case api.NodeGroupTypeManaged: - makePath := func(fieldPath string) string { - return fmt.Sprintf("%s.ManagedNodeGroup.Properties.%s", resourcesRootPath, fieldPath) - } - makeScalingPath := func(field string) string { - return makePath(fmt.Sprintf("ScalingConfig.%s", field)) - } - return &nodeGroupPaths{ - InstanceType: makePath("InstanceTypes.0"), - DesiredCapacity: makeScalingPath("DesiredSize"), - MinSize: makeScalingPath("MinSize"), - MaxSize: makeScalingPath("MaxSize"), - }, nil - - // Tag may not exist for existing nodegroups - case api.NodeGroupTypeUnmanaged, "": - makePath := func(field string) string { - return fmt.Sprintf("%s.NodeGroup.Properties.%s", resourcesRootPath, field) - } - return &nodeGroupPaths{ - InstanceType: resourcesRootPath + ".NodeGroupLaunchTemplate.Properties.LaunchTemplateData.InstanceType", - DesiredCapacity: makePath("DesiredCapacity"), - MinSize: makePath("MinSize"), - MaxSize: makePath("MaxSize"), - }, nil - - default: - return nil, fmt.Errorf("unexpected nodegroup type tag: %q", nodeGroupType) - } - -} - -func (c *StackCollection) mapStackToNodeGroupSummary(stack *Stack, ngPaths *nodeGroupPaths) (*NodeGroupSummary, error) { - template, err := c.GetStackTemplate(*stack.StackName) - if err != nil { - return nil, errors.Wrapf(err, "error getting CloudFormation template for stack %s", *stack.StackName) - } - - summary := &NodeGroupSummary{ - StackName: *stack.StackName, - Cluster: getClusterNameTag(stack), - Name: c.GetNodeGroupName(stack), - Status: *stack.StackStatus, - MaxSize: int(gjson.Get(template, ngPaths.MaxSize).Int()), - MinSize: int(gjson.Get(template, ngPaths.MinSize).Int()), - DesiredCapacity: int(gjson.Get(template, ngPaths.DesiredCapacity).Int()), - InstanceType: gjson.Get(template, ngPaths.InstanceType).String(), - ImageID: gjson.Get(template, imageIDPath).String(), - CreationTime: *stack.CreationTime, - } - - nodeGroupType, err := GetNodeGroupType(stack.Tags) - if err != nil { - return nil, err - } - - var nodeInstanceRoleARN string - if nodeGroupType == api.NodeGroupTypeUnmanaged { - nodeInstanceRoleARNCollector := func(s string) error { - nodeInstanceRoleARN = s - return nil - } - collectors := map[string]outputs.Collector{ - outputs.NodeGroupInstanceRoleARN: nodeInstanceRoleARNCollector, - } - collectorSet := outputs.NewCollectorSet(collectors) - if err := collectorSet.MustCollect(*stack); err != nil { - logger.Warning(errors.Wrapf(err, "error collecting Cloudformation outputs for stack %s", *stack.StackName).Error()) - } - } - - summary.NodeInstanceRoleARN = nodeInstanceRoleARN - - return summary, nil -} - // GetNodeGroupName will return nodegroup name based on tags func (*StackCollection) GetNodeGroupName(s *Stack) string { if tagName := GetNodegroupTagName(s.Tags); tagName != "" { diff --git a/pkg/cfn/manager/nodegroup_test.go b/pkg/cfn/manager/nodegroup_test.go index 2d45fdda3b..deae342f3f 100644 --- a/pkg/cfn/manager/nodegroup_test.go +++ b/pkg/cfn/manager/nodegroup_test.go @@ -1,231 +1,17 @@ package manager import ( - "fmt" - "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/autoscaling" cfn "github.com/aws/aws-sdk-go/service/cloudformation" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - "github.com/stretchr/testify/mock" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" ) var _ = Describe("StackCollection NodeGroup", func() { - var ( - cc *api.ClusterConfig - sc StackManager - - p *mockprovider.MockProvider - ) - - const nodegroupResource = ` -{ - "Resources": { - "NodeGroup": { - "Type": "AWS::AutoScaling::AutoScalingGroup", - "Properties": { - "DesiredCapacity": "3", - "MaxSize": "6", - "MinSize": "1" - } - } - } -} - -` - testAZs := []string{"us-west-2b", "us-west-2a", "us-west-2c"} - - newClusterConfig := func(clusterName string) *api.ClusterConfig { - cfg := api.NewClusterConfig() - - cfg.Metadata.Region = "us-west-2" - cfg.Metadata.Name = clusterName - cfg.AvailabilityZones = testAZs - - *cfg.VPC.CIDR = api.DefaultCIDR() - - return cfg - } - - newNodeGroup := func(cfg *api.ClusterConfig) { - ng := cfg.NewNodeGroup() - ng.InstanceType = "t2.medium" - ng.AMIFamily = "AmazonLinux2" - ng.Name = "12345" - } - - Describe("GetUnmanagedNodeGroupSummaries", func() { - Context("With a cluster name", func() { - var ( - clusterName string - err error - out []*NodeGroupSummary - creationTime = time.Now() - ) - - JustBeforeEach(func() { - p = mockprovider.NewMockProvider() - - cc = newClusterConfig(clusterName) - - newNodeGroup(cc) - - sc = NewStackCollection(p, cc) - - p.MockCloudFormation().On("GetTemplate", mock.MatchedBy(func(input *cfn.GetTemplateInput) bool { - return input.StackName != nil && *input.StackName == "eksctl-test-cluster-nodegroup-12345" - })).Return(&cfn.GetTemplateOutput{ - TemplateBody: aws.String(nodegroupResource), - }, nil) - - p.MockCloudFormation().On("GetTemplate", mock.Anything).Return(nil, fmt.Errorf("GetTemplate failed")) - - p.MockCloudFormation().On("ListStacksPages", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - consume := args[1].(func(p *cfn.ListStacksOutput, last bool) (shouldContinue bool)) - out := &cfn.ListStacksOutput{ - StackSummaries: []*cfn.StackSummary{ - { - StackName: aws.String("eksctl-test-cluster-nodegroup-12345"), - }, - }, - } - cont := consume(out, true) - if !cont { - panic("unexpected return value from the paging function: shouldContinue was false. It becomes false only when subsequent DescribeStacks call(s) fail, which isn't expected in this test scenario") - } - }).Return(nil) - - p.MockCloudFormation().On("DescribeStacks", mock.MatchedBy(func(input *cfn.DescribeStacksInput) bool { - return input.StackName != nil && *input.StackName == "eksctl-test-cluster-nodegroup-12345" - })).Return(&cfn.DescribeStacksOutput{ - Stacks: []*cfn.Stack{ - { - StackName: aws.String("eksctl-test-cluster-nodegroup-12345"), - StackId: aws.String("eksctl-test-cluster-nodegroup-12345-id"), - StackStatus: aws.String("CREATE_COMPLETE"), - CreationTime: aws.Time(creationTime), - Tags: []*cfn.Tag{ - { - Key: aws.String(api.NodeGroupNameTag), - Value: aws.String("12345"), - }, - { - Key: aws.String(api.ClusterNameTag), - Value: aws.String(clusterName), - }, - }, - Outputs: []*cfn.Output{ - { - OutputKey: aws.String("InstanceRoleARN"), - OutputValue: aws.String("arn:aws:iam::1111:role/eks-nodes-base-role"), - }, - }, - }, - }, - }, nil) - - p.MockCloudFormation().On("DescribeStacks", mock.Anything).Return(nil, fmt.Errorf("DescribeStacks failed")) - - p.MockCloudFormation().On("DescribeStackResource", mock.MatchedBy(func(input *cfn.DescribeStackResourceInput) bool { - return input.StackName != nil && *input.StackName == "eksctl-test-cluster-nodegroup-12345" && input.LogicalResourceId != nil && *input.LogicalResourceId == "NodeGroup" - })).Return(&cfn.DescribeStackResourceOutput{ - StackResourceDetail: &cfn.StackResourceDetail{ - PhysicalResourceId: aws.String("eksctl-test-cluster-nodegroup-12345-NodeGroup-1N68LL8H1EH27"), - }, - }, nil) - - p.MockCloudFormation().On("DescribeStackResource", mock.Anything).Return(nil, fmt.Errorf("DescribeStackResource failed")) - - p.MockASG().On("DescribeAutoScalingGroups", mock.MatchedBy(func(input *autoscaling.DescribeAutoScalingGroupsInput) bool { - return len(input.AutoScalingGroupNames) == 1 && *input.AutoScalingGroupNames[0] == "eksctl-test-cluster-nodegroup-12345-NodeGroup-1N68LL8H1EH27" - })).Return(&autoscaling.DescribeAutoScalingGroupsOutput{ - AutoScalingGroups: []*autoscaling.Group{ - { - DesiredCapacity: aws.Int64(7), - MinSize: aws.Int64(1), - MaxSize: aws.Int64(10), - }, - }, - }, nil) - - p.MockASG().On("DescribeAutoScalingGroups", mock.Anything).Return(nil, fmt.Errorf("DescribeAutoScalingGroups failed")) - }) - - Context("With no matching stacks", func() { - BeforeEach(func() { - clusterName = "test-cluster-non-existent" - }) - - JustBeforeEach(func() { - out, err = sc.GetUnmanagedNodeGroupSummaries("") - }) - - It("should not error", func() { - Expect(err).NotTo(HaveOccurred()) - }) - - It("should not have called AWS CloudFormation GetTemplate", func() { - Expect(p.MockCloudFormation().AssertNumberOfCalls(GinkgoT(), "GetTemplate", 0)).To(BeTrue()) - }) - - It("the output should equal the expectation", func() { - Expect(out).To(HaveLen(0)) - }) - }) - - Context("With matching stacks", func() { - BeforeEach(func() { - clusterName = "test-cluster" - }) - - JustBeforeEach(func() { - out, err = sc.GetUnmanagedNodeGroupSummaries("") - }) - - It("should not error", func() { - Expect(err).NotTo(HaveOccurred()) - }) - - It("should have called AWS CloudFormation GetTemplate", func() { - Expect(p.MockCloudFormation().AssertNumberOfCalls(GinkgoT(), "GetTemplate", 1)).To(BeTrue()) - }) - - It("should have called AWS CloudFormation DescribeStacks once", func() { - Expect(p.MockCloudFormation().AssertNumberOfCalls(GinkgoT(), "DescribeStacks", 1)).To(BeTrue()) - }) - - It("the output should equal the expectation", func() { - Expect(out).To(HaveLen(1)) - summary := *out[0] - Expect(summary).To(Equal(NodeGroupSummary{ - StackName: "eksctl-test-cluster-nodegroup-12345", - Cluster: clusterName, - Name: "12345", - Status: "CREATE_COMPLETE", - MaxSize: 10, - MinSize: 1, - DesiredCapacity: 7, - InstanceType: "", - ImageID: "", - CreationTime: creationTime, - NodeInstanceRoleARN: "arn:aws:iam::1111:role/eks-nodes-base-role", - AutoScalingGroupName: "eksctl-test-cluster-nodegroup-12345-NodeGroup-1N68LL8H1EH27", - Version: "", - NodeGroupType: "unmanaged", - })) - }) - }) - }) - }) - Describe("GetNodeGroupType", func() { - createTags := func(tags map[string]string) []*cfn.Tag { cfnTags := make([]*cfn.Tag, 0) for k, v := range tags { diff --git a/pkg/ctl/get/nodegroup.go b/pkg/ctl/get/nodegroup.go index 514bb278f3..6446977c8f 100644 --- a/pkg/ctl/get/nodegroup.go +++ b/pkg/ctl/get/nodegroup.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/pflag" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" "github.com/weaveworks/eksctl/pkg/printers" ) @@ -70,7 +69,7 @@ func doGetNodeGroup(cmd *cmdutils.Cmd, ng *api.NodeGroup, params *getCmdParams) return err } - var summaries []*manager.NodeGroupSummary + var summaries []*nodegroup.Summary if ng.Name == "" { summaries, err = nodegroup.New(cfg, ctl, clientSet).GetAll() if err != nil { @@ -106,37 +105,37 @@ func doGetNodeGroup(cmd *cmdutils.Cmd, ng *api.NodeGroup, params *getCmdParams) } func addSummaryTableColumns(printer *printers.TablePrinter) { - printer.AddColumn("CLUSTER", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("CLUSTER", func(s *nodegroup.Summary) string { return s.Cluster }) - printer.AddColumn("NODEGROUP", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("NODEGROUP", func(s *nodegroup.Summary) string { return s.Name }) - printer.AddColumn("STATUS", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("STATUS", func(s *nodegroup.Summary) string { return s.Status }) - printer.AddColumn("CREATED", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("CREATED", func(s *nodegroup.Summary) string { return s.CreationTime.Format(time.RFC3339) }) - printer.AddColumn("MIN SIZE", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("MIN SIZE", func(s *nodegroup.Summary) string { return strconv.Itoa(s.MinSize) }) - printer.AddColumn("MAX SIZE", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("MAX SIZE", func(s *nodegroup.Summary) string { return strconv.Itoa(s.MaxSize) }) - printer.AddColumn("DESIRED CAPACITY", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("DESIRED CAPACITY", func(s *nodegroup.Summary) string { return strconv.Itoa(s.DesiredCapacity) }) - printer.AddColumn("INSTANCE TYPE", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("INSTANCE TYPE", func(s *nodegroup.Summary) string { return s.InstanceType }) - printer.AddColumn("IMAGE ID", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("IMAGE ID", func(s *nodegroup.Summary) string { return s.ImageID }) - printer.AddColumn("ASG NAME", func(s *manager.NodeGroupSummary) string { + printer.AddColumn("ASG NAME", func(s *nodegroup.Summary) string { return s.AutoScalingGroupName }) - printer.AddColumn("TYPE", func(s *manager.NodeGroupSummary) api.NodeGroupType { + printer.AddColumn("TYPE", func(s *nodegroup.Summary) api.NodeGroupType { return s.NodeGroupType }) }