diff --git a/Makefile b/Makefile index 947ba7e646..121e804348 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,7 @@ generate-always: pkg/addons/default/assets/aws-node.yaml ## Generate code (requi # mocks go generate ./pkg/eks go generate ./pkg/drain + go generate ./pkg/actions/... .PHONY: generate-all generate-all: generate-always $(conditionally_generated_files) ## Re-generate all the automatically-generated source files diff --git a/go.mod b/go.mod index 5945cfc644..ac4b84cb5b 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/kubicorn/kubicorn v0.0.0-20180829191017-06f6bce92acc github.com/lithammer/dedent v1.1.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0 + github.com/miekg/coredns v0.0.0-20161111164017-20e25559d5ea github.com/onsi/ginkgo v1.14.2 github.com/onsi/gomega v1.10.4 github.com/pelletier/go-toml v1.8.1 diff --git a/go.sum b/go.sum index 211c5eb343..7cdafc0d5a 100644 --- a/go.sum +++ b/go.sum @@ -203,6 +203,7 @@ github.com/aws/aws-sdk-go v1.35.10/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9 github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.36.31 h1:BMVngapDGAfLBVEVzaSIw3fmJdWx7jOvhLCXgRXbXQI= github.com/aws/aws-sdk-go v1.36.31/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.36.33 h1:ASmYIgWuPW1p01Xxch3ygaptshrEe7Vt+CirmwIqMtI= github.com/awslabs/goformation/v4 v4.15.5 h1:q3lm7oj4yqqJ76ZcaFThUACT3MQLD6yBcJRKuZ6g87w= github.com/awslabs/goformation/v4 v4.15.5/go.mod h1:wB5lKZf1J0MYH1Lt4B9w3opqz0uIjP7MMCAcib3QkwA= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= @@ -943,9 +944,11 @@ github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2 github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/miekg/coredns v0.0.0-20161111164017-20e25559d5ea h1:ClxQqQsf07a0/3NsMYizr/dMxQoeSpNWDge0v3iHEcU= github.com/miekg/coredns v0.0.0-20161111164017-20e25559d5ea/go.mod h1:ulj34RFTnjlzXt4MMq5AcKBIiXNiru0D2fe3enowwU4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mindprince/gonvml v0.0.0-20190828220739-9ebdce4bb989/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= diff --git a/integration/tests/params.go b/integration/tests/params.go index 22bf485284..d3d9488343 100644 --- a/integration/tests/params.go +++ b/integration/tests/params.go @@ -33,6 +33,8 @@ type Params struct { EksctlUpgradeCmd runner.Cmd EksctlUpdateCmd runner.Cmd EksctlGetCmd runner.Cmd + EksctlSetLabelsCmd runner.Cmd + EksctlUnsetLabelsCmd runner.Cmd EksctlDeleteCmd runner.Cmd EksctlDeleteClusterCmd runner.Cmd EksctlDrainNodeGroupCmd runner.Cmd @@ -77,6 +79,14 @@ func (p *Params) GenerateCommands() { WithArgs("get"). WithTimeout(1 * time.Minute) + p.EksctlSetLabelsCmd = p.EksctlCmd. + WithArgs("set", "labels"). + WithTimeout(1 * time.Minute) + + p.EksctlUnsetLabelsCmd = p.EksctlCmd. + WithArgs("unset", "labels"). + WithTimeout(1 * time.Minute) + p.EksctlDeleteCmd = p.EksctlCmd. WithArgs("delete"). WithTimeout(15 * time.Minute) diff --git a/integration/tests/unowned_cluster/unowned_cluster_test.go b/integration/tests/unowned_cluster/unowned_cluster_test.go index de5ae0a316..c288523a41 100644 --- a/integration/tests/unowned_cluster/unowned_cluster_test.go +++ b/integration/tests/unowned_cluster/unowned_cluster_test.go @@ -90,6 +90,38 @@ var _ = Describe("(Integration) [non-eksctl cluster & nodegroup support]", func( ContainElement(ContainSubstring("ng-2")), )) + By("Setting labels on a nodegroup") + cmd = params.EksctlSetLabelsCmd. + WithArgs( + "--cluster", clusterName, + "--nodegroup", ng1, + "--labels", "key=value", + "--verbose", "2", + ) + Expect(cmd).To(RunSuccessfully()) + + By("Getting labels for a nodegroup") + cmd = params.EksctlGetCmd. + WithArgs( + "labels", + "--cluster", clusterName, + "--nodegroup", ng1, + "--verbose", "2", + ) + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring("key=value")), + )) + + By("Unsetting labels on a nodegroup") + cmd = params.EksctlUnsetLabelsCmd. + WithArgs( + "--cluster", clusterName, + "--nodegroup", ng1, + "--labels", "key", + "--verbose", "2", + ) + Expect(cmd).To(RunSuccessfully()) + By("Enabling OIDC") cmd = params.EksctlUtilsCmd. WithArgs( @@ -99,6 +131,7 @@ var _ = Describe("(Integration) [non-eksctl cluster & nodegroup support]", func( "--verbose", "2", ) Expect(cmd).To(RunSuccessfully()) + By("Creating an IAMServiceAccount") cmd = params.EksctlCreateCmd. WithArgs( @@ -112,6 +145,7 @@ var _ = Describe("(Integration) [non-eksctl cluster & nodegroup support]", func( "--verbose", "2", ) Expect(cmd).To(RunSuccessfully()) + By("Getting IAMServiceAccounts") cmd = params.EksctlGetCmd. WithArgs( diff --git a/pkg/actions/addon/fakes/fake_stack_manager.go b/pkg/actions/addon/fakes/fake_stack_manager.go index c7da8084f5..ee8393013b 100644 --- a/pkg/actions/addon/fakes/fake_stack_manager.go +++ b/pkg/actions/addon/fakes/fake_stack_manager.go @@ -82,15 +82,16 @@ func (fake *FakeStackManager) CreateStack(arg1 string, arg2 builder.ResourceSet, arg4 map[string]string arg5 chan error }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.CreateStackStub + fakeReturns := fake.createStackReturns fake.recordInvocation("CreateStack", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.createStackMutex.Unlock() - if fake.CreateStackStub != nil { - return fake.CreateStackStub(arg1, arg2, arg3, arg4, arg5) + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 } - fakeReturns := fake.createStackReturns return fakeReturns.result1 } @@ -142,15 +143,16 @@ func (fake *FakeStackManager) DeleteStackByName(arg1 string) (*cloudformation.St fake.deleteStackByNameArgsForCall = append(fake.deleteStackByNameArgsForCall, struct { arg1 string }{arg1}) + stub := fake.DeleteStackByNameStub + fakeReturns := fake.deleteStackByNameReturns fake.recordInvocation("DeleteStackByName", []interface{}{arg1}) fake.deleteStackByNameMutex.Unlock() - if fake.DeleteStackByNameStub != nil { - return fake.DeleteStackByNameStub(arg1) + if stub != nil { + return stub(arg1) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.deleteStackByNameReturns return fakeReturns.result1, fakeReturns.result2 } @@ -206,15 +208,16 @@ func (fake *FakeStackManager) ListStacksMatching(arg1 string, arg2 ...string) ([ arg1 string arg2 []string }{arg1, arg2}) + stub := fake.ListStacksMatchingStub + fakeReturns := fake.listStacksMatchingReturns fake.recordInvocation("ListStacksMatching", []interface{}{arg1, arg2}) fake.listStacksMatchingMutex.Unlock() - if fake.ListStacksMatchingStub != nil { - return fake.ListStacksMatchingStub(arg1, arg2...) + if stub != nil { + return stub(arg1, arg2...) } if specificReturn { return ret.result1, ret.result2 } - fakeReturns := fake.listStacksMatchingReturns return fakeReturns.result1, fakeReturns.result2 } @@ -273,15 +276,16 @@ func (fake *FakeStackManager) UpdateStack(arg1 string, arg2 string, arg3 string, arg4 manager.TemplateData arg5 map[string]string }{arg1, arg2, arg3, arg4, arg5}) + stub := fake.UpdateStackStub + fakeReturns := fake.updateStackReturns fake.recordInvocation("UpdateStack", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.updateStackMutex.Unlock() - if fake.UpdateStackStub != nil { - return fake.UpdateStackStub(arg1, arg2, arg3, arg4, arg5) + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 } - fakeReturns := fake.updateStackReturns return fakeReturns.result1 } diff --git a/pkg/actions/label/export_test.go b/pkg/actions/label/export_test.go new file mode 100644 index 0000000000..d86ccb6e02 --- /dev/null +++ b/pkg/actions/label/export_test.go @@ -0,0 +1,5 @@ +package label + +func (m *Manager) SetService(service Service) { + m.service = service +} diff --git a/pkg/actions/label/fakes/fake_managed_service.go b/pkg/actions/label/fakes/fake_managed_service.go new file mode 100644 index 0000000000..9a45f1d813 --- /dev/null +++ b/pkg/actions/label/fakes/fake_managed_service.go @@ -0,0 +1,199 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "sync" + + "github.com/weaveworks/eksctl/pkg/actions/label" +) + +type FakeService struct { + GetLabelsStub func(string) (map[string]string, error) + getLabelsMutex sync.RWMutex + getLabelsArgsForCall []struct { + arg1 string + } + getLabelsReturns struct { + result1 map[string]string + result2 error + } + getLabelsReturnsOnCall map[int]struct { + result1 map[string]string + result2 error + } + UpdateLabelsStub func(string, map[string]string, []string) error + updateLabelsMutex sync.RWMutex + updateLabelsArgsForCall []struct { + arg1 string + arg2 map[string]string + arg3 []string + } + updateLabelsReturns struct { + result1 error + } + updateLabelsReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeService) GetLabels(arg1 string) (map[string]string, error) { + fake.getLabelsMutex.Lock() + ret, specificReturn := fake.getLabelsReturnsOnCall[len(fake.getLabelsArgsForCall)] + fake.getLabelsArgsForCall = append(fake.getLabelsArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.GetLabelsStub + fakeReturns := fake.getLabelsReturns + fake.recordInvocation("GetLabels", []interface{}{arg1}) + fake.getLabelsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeService) GetLabelsCallCount() int { + fake.getLabelsMutex.RLock() + defer fake.getLabelsMutex.RUnlock() + return len(fake.getLabelsArgsForCall) +} + +func (fake *FakeService) GetLabelsCalls(stub func(string) (map[string]string, error)) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = stub +} + +func (fake *FakeService) GetLabelsArgsForCall(i int) string { + fake.getLabelsMutex.RLock() + defer fake.getLabelsMutex.RUnlock() + argsForCall := fake.getLabelsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeService) GetLabelsReturns(result1 map[string]string, result2 error) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = nil + fake.getLabelsReturns = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeService) GetLabelsReturnsOnCall(i int, result1 map[string]string, result2 error) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = nil + if fake.getLabelsReturnsOnCall == nil { + fake.getLabelsReturnsOnCall = make(map[int]struct { + result1 map[string]string + result2 error + }) + } + fake.getLabelsReturnsOnCall[i] = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeService) UpdateLabels(arg1 string, arg2 map[string]string, arg3 []string) error { + var arg3Copy []string + if arg3 != nil { + arg3Copy = make([]string, len(arg3)) + copy(arg3Copy, arg3) + } + fake.updateLabelsMutex.Lock() + ret, specificReturn := fake.updateLabelsReturnsOnCall[len(fake.updateLabelsArgsForCall)] + fake.updateLabelsArgsForCall = append(fake.updateLabelsArgsForCall, struct { + arg1 string + arg2 map[string]string + arg3 []string + }{arg1, arg2, arg3Copy}) + stub := fake.UpdateLabelsStub + fakeReturns := fake.updateLabelsReturns + fake.recordInvocation("UpdateLabels", []interface{}{arg1, arg2, arg3Copy}) + fake.updateLabelsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeService) UpdateLabelsCallCount() int { + fake.updateLabelsMutex.RLock() + defer fake.updateLabelsMutex.RUnlock() + return len(fake.updateLabelsArgsForCall) +} + +func (fake *FakeService) UpdateLabelsCalls(stub func(string, map[string]string, []string) error) { + fake.updateLabelsMutex.Lock() + defer fake.updateLabelsMutex.Unlock() + fake.UpdateLabelsStub = stub +} + +func (fake *FakeService) UpdateLabelsArgsForCall(i int) (string, map[string]string, []string) { + fake.updateLabelsMutex.RLock() + defer fake.updateLabelsMutex.RUnlock() + argsForCall := fake.updateLabelsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeService) UpdateLabelsReturns(result1 error) { + fake.updateLabelsMutex.Lock() + defer fake.updateLabelsMutex.Unlock() + fake.UpdateLabelsStub = nil + fake.updateLabelsReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeService) UpdateLabelsReturnsOnCall(i int, result1 error) { + fake.updateLabelsMutex.Lock() + defer fake.updateLabelsMutex.Unlock() + fake.UpdateLabelsStub = nil + if fake.updateLabelsReturnsOnCall == nil { + fake.updateLabelsReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateLabelsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeService) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getLabelsMutex.RLock() + defer fake.getLabelsMutex.RUnlock() + fake.updateLabelsMutex.RLock() + defer fake.updateLabelsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeService) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ label.Service = new(FakeService) diff --git a/pkg/actions/label/get.go b/pkg/actions/label/get.go new file mode 100644 index 0000000000..fcee9472e6 --- /dev/null +++ b/pkg/actions/label/get.go @@ -0,0 +1,57 @@ +package label + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" +) + +type Summary struct { + Cluster string + NodeGroup string + Labels map[string]string +} + +func (m *Manager) Get(nodeGroupName string) ([]Summary, error) { + var ( + labels map[string]string + err error + ) + + labels, err = m.service.GetLabels(nodeGroupName) + if err != nil { + switch { + case isValidationError(err): + labels, err = m.getLabelsFromUnownedNodeGroup(nodeGroupName) + if err != nil { + return nil, err + } + default: + return nil, err + } + } + + return []Summary{ + { + Cluster: m.clusterName, + NodeGroup: nodeGroupName, + Labels: labels, + }, + }, nil +} + +func (m *Manager) getLabelsFromUnownedNodeGroup(nodeGroupName string) (map[string]string, error) { + out, err := m.eksAPI.DescribeNodegroup(&eks.DescribeNodegroupInput{ + ClusterName: aws.String(m.clusterName), + NodegroupName: aws.String(nodeGroupName), + }) + if err != nil { + return nil, err + } + + labels := make(map[string]string, len(out.Nodegroup.Labels)) + for k, v := range out.Nodegroup.Labels { + labels[k] = *v + } + + return labels, nil +} diff --git a/pkg/actions/label/label.go b/pkg/actions/label/label.go new file mode 100644 index 0000000000..c430bb7f56 --- /dev/null +++ b/pkg/actions/label/label.go @@ -0,0 +1,43 @@ +package label + +import ( + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/eks/eksiface" + "github.com/pkg/errors" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/eks" + "github.com/weaveworks/eksctl/pkg/managed" +) + +//go:generate counterfeiter -o fakes/fake_managed_service.go . Service +type Service interface { + GetLabels(nodeGroupName string) (map[string]string, error) + UpdateLabels(nodeGroupName string, labelsToAdd map[string]string, labelsToRemove []string) error +} + +type Manager struct { + service Service + eksAPI eksiface.EKSAPI + clusterName string +} + +func New(cfg *api.ClusterConfig, ctl *eks.ClusterProvider) *Manager { + sc := manager.NewStackCollection(ctl.Provider, cfg) + mc := managed.NewService(ctl.Provider, sc, cfg.Metadata.Name) + return &Manager{ + service: mc, + eksAPI: ctl.Provider.EKS(), + clusterName: cfg.Metadata.Name, + } +} + +// If a ValidationError code is returned then an eksctl-marked stack was not +// found for that nodegroup so we can then try to call the EKS api directly. +func isValidationError(err error) bool { + awsErr, ok := errors.Cause(err).(awserr.Error) + if !ok { + return false + } + return awsErr.Code() == "ValidationError" +} diff --git a/pkg/actions/label/label_suite_test.go b/pkg/actions/label/label_suite_test.go new file mode 100644 index 0000000000..2f3ed42407 --- /dev/null +++ b/pkg/actions/label/label_suite_test.go @@ -0,0 +1,13 @@ +package label_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestLabel(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Label Suite") +} diff --git a/pkg/actions/label/labels_test.go b/pkg/actions/label/labels_test.go new file mode 100644 index 0000000000..4ca6730d4a --- /dev/null +++ b/pkg/actions/label/labels_test.go @@ -0,0 +1,235 @@ +package label_test + +import ( + "errors" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awseks "github.com/aws/aws-sdk-go/service/eks" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/mock" + "github.com/weaveworks/eksctl/pkg/actions/label" + "github.com/weaveworks/eksctl/pkg/actions/label/fakes" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/eks" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" +) + +var _ = Describe("Labels", func() { + var ( + fakeManagedService *fakes.FakeService + mockProvider *mockprovider.MockProvider + cfg *api.ClusterConfig + manager *label.Manager + + clusterName string + nodegroupName string + ) + + BeforeEach(func() { + fakeManagedService = new(fakes.FakeService) + mockProvider = mockprovider.NewMockProvider() + clusterName = "foo" + nodegroupName = "bar" + cfg = &api.ClusterConfig{ + Metadata: &api.ClusterMeta{ + Name: clusterName, + }, + } + manager = label.New(cfg, &eks.ClusterProvider{Provider: mockProvider}) + manager.SetService(fakeManagedService) + }) + + Describe("Get", func() { + var expectedLabels map[string]string + + BeforeEach(func() { + expectedLabels = map[string]string{"k1": "v1"} + }) + + When("the nodegroup is owned by eksctl", func() { + BeforeEach(func() { + fakeManagedService.GetLabelsReturns(expectedLabels, nil) + }) + + It("returns the labels from the nodegroup stack", func() { + summary, err := manager.Get(nodegroupName) + Expect(err).NotTo(HaveOccurred()) + Expect(summary[0].Labels).To(Equal(expectedLabels)) + }) + + When("the service returns an error", func() { + BeforeEach(func() { + fakeManagedService.GetLabelsReturns(nil, errors.New("something-terrible")) + }) + + It("fails", func() { + summary, err := manager.Get(nodegroupName) + Expect(err).To(HaveOccurred()) + Expect(summary).To(BeNil()) + }) + }) + }) + + When("the nodegroup is not owned by eksctl", func() { + var returnedLabels map[string]*string + + BeforeEach(func() { + returnedLabels = map[string]*string{"k1": aws.String("v1")} + fakeManagedService.GetLabelsReturns(nil, awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("returns the labels from the EKS api", func() { + mockProvider.MockEKS().On("DescribeNodegroup", &awseks.DescribeNodegroupInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(nodegroupName), + }).Return(&awseks.DescribeNodegroupOutput{Nodegroup: &awseks.Nodegroup{Labels: returnedLabels}}, nil) + + summary, err := manager.Get(nodegroupName) + Expect(err).NotTo(HaveOccurred()) + Expect(summary[0].Labels).To(Equal(expectedLabels)) + }) + + When("the EKS api returns an error", func() { + BeforeEach(func() { + fakeManagedService.GetLabelsReturns(nil, awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("fails", func() { + mockProvider.MockEKS().On("DescribeNodegroup", mock.Anything).Return(&awseks.DescribeNodegroupOutput{}, errors.New("oh-noes")) + + summary, err := manager.Get(nodegroupName) + Expect(err).To(HaveOccurred()) + Expect(summary).To(BeNil()) + }) + }) + }) + }) + + Describe("Set", func() { + var labels map[string]string + + BeforeEach(func() { + labels = map[string]string{"k1": "v1"} + }) + + When("the nodegroup is owned by eksctl", func() { + BeforeEach(func() { + fakeManagedService.UpdateLabelsReturns(nil) + }) + + It("sets new labels by updating the nodegroup stack", func() { + Expect(manager.Set(nodegroupName, labels)).To(Succeed()) + }) + + When("the service returns an error", func() { + BeforeEach(func() { + fakeManagedService.UpdateLabelsReturns(errors.New("something-terrible")) + }) + + It("fails", func() { + err := manager.Set(nodegroupName, labels) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + When("the nodegroup is not owned by eksctl", func() { + var eksLabels map[string]*string + + BeforeEach(func() { + eksLabels = map[string]*string{"k1": aws.String("v1")} + fakeManagedService.UpdateLabelsReturns(awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("updates the labels through the EKS api", func() { + mockProvider.MockEKS().On("UpdateNodegroupConfig", &awseks.UpdateNodegroupConfigInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(nodegroupName), + Labels: &awseks.UpdateLabelsPayload{ + AddOrUpdateLabels: eksLabels, + }, + }).Return(&awseks.UpdateNodegroupConfigOutput{}, nil) + + Expect(manager.Set(nodegroupName, labels)).To(Succeed()) + }) + + When("the EKS api returns an error", func() { + BeforeEach(func() { + fakeManagedService.GetLabelsReturns(nil, awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("fails", func() { + mockProvider.MockEKS().On("UpdateNodegroupConfig", mock.Anything).Return(&awseks.UpdateNodegroupConfigOutput{}, errors.New("oh-noes")) + + err := manager.Set(nodegroupName, labels) + Expect(err).To(HaveOccurred()) + }) + }) + }) + }) + + Describe("Unset", func() { + var labels []string + + BeforeEach(func() { + labels = []string{"k1"} + }) + + When("the nodegroup is owned by eksctl", func() { + BeforeEach(func() { + fakeManagedService.UpdateLabelsReturns(nil) + }) + + It("removes labels by updating the nodegroup stack", func() { + Expect(manager.Unset(nodegroupName, labels)).To(Succeed()) + }) + + When("the service returns an error", func() { + BeforeEach(func() { + fakeManagedService.UpdateLabelsReturns(errors.New("something-terrible")) + }) + + It("fails", func() { + err := manager.Unset(nodegroupName, labels) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + When("the nodegroup is not owned by eksctl", func() { + var eksLabels []*string + + BeforeEach(func() { + eksLabels = []*string{aws.String("k1")} + fakeManagedService.UpdateLabelsReturns(awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("removes the labels through the EKS api", func() { + mockProvider.MockEKS().On("UpdateNodegroupConfig", &awseks.UpdateNodegroupConfigInput{ + ClusterName: aws.String(clusterName), + NodegroupName: aws.String(nodegroupName), + Labels: &awseks.UpdateLabelsPayload{ + RemoveLabels: eksLabels, + }, + }).Return(&awseks.UpdateNodegroupConfigOutput{}, nil) + + Expect(manager.Unset(nodegroupName, labels)).To(Succeed()) + }) + + When("the EKS api returns an error", func() { + BeforeEach(func() { + fakeManagedService.GetLabelsReturns(nil, awserr.New("ValidationError", "stack not found", errors.New("omg"))) + }) + + It("fails", func() { + mockProvider.MockEKS().On("UpdateNodegroupConfig", mock.Anything).Return(&awseks.UpdateNodegroupConfigOutput{}, errors.New("oh-noes")) + + err := manager.Unset(nodegroupName, labels) + Expect(err).To(HaveOccurred()) + }) + }) + }) + }) +}) diff --git a/pkg/actions/label/set.go b/pkg/actions/label/set.go new file mode 100644 index 0000000000..23ebda7de6 --- /dev/null +++ b/pkg/actions/label/set.go @@ -0,0 +1,28 @@ +package label + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" +) + +func (m *Manager) Set(nodeGroupName string, labels map[string]string) error { + err := m.service.UpdateLabels(nodeGroupName, labels, nil) + if err != nil && isValidationError(err) { + return m.setLabelsOnUnownedNodeGroup(nodeGroupName, labels) + } + return err +} + +func (m *Manager) setLabelsOnUnownedNodeGroup(nodeGroupName string, labels map[string]string) error { + pointyLabels := aws.StringMap(labels) + _, err := m.eksAPI.UpdateNodegroupConfig(&eks.UpdateNodegroupConfigInput{ + ClusterName: aws.String(m.clusterName), + NodegroupName: aws.String(nodeGroupName), + Labels: &eks.UpdateLabelsPayload{AddOrUpdateLabels: pointyLabels}, + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/actions/label/unset.go b/pkg/actions/label/unset.go new file mode 100644 index 0000000000..a52eecaf00 --- /dev/null +++ b/pkg/actions/label/unset.go @@ -0,0 +1,36 @@ +package label + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" +) + +func (m *Manager) Unset(nodeGroupName string, labels []string) error { + err := m.service.UpdateLabels(nodeGroupName, nil, labels) + if err != nil { + switch { + case isValidationError(err): + return m.unsetLabelsOnUnownedNodeGroup(nodeGroupName, labels) + default: + return err + } + } + return nil +} + +func (m *Manager) unsetLabelsOnUnownedNodeGroup(nodeGroupName string, labels []string) error { + var pointyLabels []*string + for _, v := range labels { + pointyLabels = append(pointyLabels, &v) + } + _, err := m.eksAPI.UpdateNodegroupConfig(&eks.UpdateNodegroupConfigInput{ + ClusterName: aws.String(m.clusterName), + NodegroupName: aws.String(nodeGroupName), + Labels: &eks.UpdateLabelsPayload{RemoveLabels: pointyLabels}, + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index b3e864fee7..65511db16e 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -225,7 +225,7 @@ func (c *StackCollection) GetManagedNodeGroupTemplate(nodeGroupName string) (str // UpdateNodeGroupStack updates the nodegroup stack with the specified template func (c *StackCollection) UpdateNodeGroupStack(nodeGroupName, template string) error { stackName := c.makeNodeGroupStackName(nodeGroupName) - return c.UpdateStack(stackName, c.MakeChangeSetName("update-nodegroup"), "Update nodegroup stack", TemplateBody(template), nil) + return c.UpdateStack(stackName, c.MakeChangeSetName("update-nodegroup"), "updating nodegroup stack", TemplateBody(template), nil) } // ListStacksMatching gets all of CloudFormation stacks with names matching nameRegex. diff --git a/pkg/ctl/get/labels.go b/pkg/ctl/get/labels.go index 7625f367c1..7f586518de 100644 --- a/pkg/ctl/get/labels.go +++ b/pkg/ctl/get/labels.go @@ -5,8 +5,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/weaveworks/eksctl/pkg/cfn/manager" - "github.com/weaveworks/eksctl/pkg/managed" + "github.com/weaveworks/eksctl/pkg/actions/label" "github.com/weaveworks/eksctl/pkg/printers" "k8s.io/apimachinery/pkg/labels" @@ -19,7 +18,7 @@ func getLabelsCmd(cmd *cmdutils.Cmd) { cfg := api.NewClusterConfig() cmd.ClusterConfig = cfg - cmd.SetDescription("labels", "Get nodegroup labels", "") + cmd.SetDescription("labels", "Get labels for managed nodegroup", "") var nodeGroupName string cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { @@ -39,17 +38,14 @@ func getLabelsCmd(cmd *cmdutils.Cmd) { } -type summary struct { - Cluster string - NodeGroup string - Labels map[string]string -} - func getLabels(cmd *cmdutils.Cmd, nodeGroupName string) error { cfg := cmd.ClusterConfig if cfg.Metadata.Name == "" { return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) } + if nodeGroupName == "" { + return cmdutils.ErrMustBeSet("--nodegroup") + } if cmd.NameArg != "" { return cmdutils.ErrUnsupportedNameArg() @@ -63,34 +59,25 @@ func getLabels(cmd *cmdutils.Cmd, nodeGroupName string) error { cmdutils.LogRegionAndVersionInfo(cmd.ClusterConfig.Metadata) - stackCollection := manager.NewStackCollection(ctl.Provider, cfg) - managedService := managed.NewService(ctl.Provider, stackCollection, cfg.Metadata.Name) - ngLabels, err := managedService.GetLabels(nodeGroupName) + manager := label.New(cfg, ctl) + labels, err := manager.Get(nodeGroupName) if err != nil { return err } - out := []summary{ - { - Cluster: cfg.Metadata.Name, - NodeGroup: nodeGroupName, - Labels: ngLabels, - }, - } - printer := printers.NewTablePrinter() addColumns(printer.(*printers.TablePrinter)) - return printer.PrintObjWithKind("labels", out, os.Stdout) + return printer.PrintObjWithKind("labels", labels, os.Stdout) } func addColumns(printer *printers.TablePrinter) { - printer.AddColumn("CLUSTER", func(s summary) string { + printer.AddColumn("CLUSTER", func(s label.Summary) string { return s.Cluster }) - printer.AddColumn("NODEGROUP", func(s summary) string { + printer.AddColumn("NODEGROUP", func(s label.Summary) string { return s.NodeGroup }) - printer.AddColumn("LABELS", func(s summary) string { + printer.AddColumn("LABELS", func(s label.Summary) string { return labels.FormatLabels(s.Labels) }) } diff --git a/pkg/ctl/get/labels_test.go b/pkg/ctl/get/labels_test.go index da77ec08c7..3a2ea17b7a 100644 --- a/pkg/ctl/get/labels_test.go +++ b/pkg/ctl/get/labels_test.go @@ -7,22 +7,29 @@ import ( var _ = Describe("get", func() { Describe("labels", func() { - It("missing required flag --cluster", func() { + It("fails when no flags set", func() { cmd := newMockCmd("labels") _, err := cmd.execute() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Error: --cluster must be set")) }) - It("missing required flag --cluster, but with --nodegroup", func() { + It("fails when --cluster flag not set", func() { cmd := newMockCmd("labels", "--nodegroup", "dummyNodeGroup") _, err := cmd.execute() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Error: --cluster must be set")) }) - It("setting name argument", func() { - cmd := newMockCmd("labels", "--cluster", "dummy", "dummyName") + It("fails when --nodegroup flag not set", func() { + cmd := newMockCmd("labels", "--cluster", "dummy") + _, err := cmd.execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Error: --nodegroup must be set")) + }) + + It("fails when name argument is used", func() { + cmd := newMockCmd("labels", "--cluster", "dummy", "--nodegroup", "dummyNodeGroup", "dummyName") _, err := cmd.execute() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Error: name argument is not supported")) diff --git a/pkg/ctl/set/labels.go b/pkg/ctl/set/labels.go index 14d7f10cf9..5619678d75 100644 --- a/pkg/ctl/set/labels.go +++ b/pkg/ctl/set/labels.go @@ -1,10 +1,10 @@ package set import ( + "github.com/kris-nova/logger" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/weaveworks/eksctl/pkg/cfn/manager" - "github.com/weaveworks/eksctl/pkg/managed" + "github.com/weaveworks/eksctl/pkg/actions/label" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" @@ -20,7 +20,7 @@ func setLabelsCmd(cmd *cmdutils.Cmd) { cfg := api.NewClusterConfig() cmd.ClusterConfig = cfg - cmd.SetDescription("labels", "Create or overwrite labels", "") + cmd.SetDescription("labels", "Create or overwrite labels for managed nodegroups", "") var options labelOptions cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { @@ -47,6 +47,9 @@ func setLabels(cmd *cmdutils.Cmd, options labelOptions) error { if cfg.Metadata.Name == "" { return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) } + if options.nodeGroupName == "" { + return cmdutils.ErrMustBeSet("--nodegroup") + } if cmd.NameArg != "" { return cmdutils.ErrUnsupportedNameArg() @@ -58,7 +61,14 @@ func setLabels(cmd *cmdutils.Cmd, options labelOptions) error { return err } - stackCollection := manager.NewStackCollection(ctl.Provider, cfg) - managedService := managed.NewService(ctl.Provider, stackCollection, cfg.Metadata.Name) - return managedService.UpdateLabels(options.nodeGroupName, options.labels, nil) + cmdutils.LogRegionAndVersionInfo(cmd.ClusterConfig.Metadata) + logger.Info("setting label(s) on nodegroup %s in cluster %s", options.nodeGroupName, cmd.ClusterConfig.Metadata) + + manager := label.New(cfg, ctl) + if err := manager.Set(options.nodeGroupName, options.labels); err != nil { + return err + } + + logger.Info("done") + return nil } diff --git a/pkg/ctl/set/set_test.go b/pkg/ctl/set/set_test.go index 9915e58869..70f253a630 100644 --- a/pkg/ctl/set/set_test.go +++ b/pkg/ctl/set/set_test.go @@ -12,26 +12,42 @@ import ( var _ = Describe("set", func() { Describe("invalid-resource", func() { - It("with no flag", func() { + It("fails", func() { cmd := newMockCmd("invalid-resource") _, err := cmd.execute() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"set\"")) Expect(err.Error()).To(ContainSubstring("usage")) }) - It("with invalid-resource and some flag", func() { - cmd := newMockCmd("invalid-resource", "--invalid-flag", "foo") + }) + + Describe("labels", func() { + It("fails when no flags set", func() { + cmd := newMockCmd("labels") _, err := cmd.execute() Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"set\"")) - Expect(err.Error()).To(ContainSubstring("usage")) + Expect(err.Error()).To(ContainSubstring("Error: required flag(s) \"labels\" not set")) }) - It("with invalid-resource and additional argument", func() { - cmd := newMockCmd("invalid-resource", "foo") + + It("fails when cluster flag not set", func() { + cmd := newMockCmd("labels", "-l", "k=v") _, err := cmd.execute() Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"set\"")) - Expect(err.Error()).To(ContainSubstring("usage")) + Expect(err.Error()).To(ContainSubstring("Error: --cluster must be set")) + }) + + It("fails when --nodegroup flag not set", func() { + cmd := newMockCmd("labels", "--cluster", "dummy", "-l", "k=v") + _, err := cmd.execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Error: --nodegroup must be set")) + }) + + It("fails when name argument is used", func() { + cmd := newMockCmd("labels", "--cluster", "dummy", "--nodegroup", "dummyNodeGroup", "dummyName", "-l", "k=v") + _, err := cmd.execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Error: name argument is not supported")) }) }) }) diff --git a/pkg/ctl/unset/labels.go b/pkg/ctl/unset/labels.go index 4911bc605a..f53084c49a 100644 --- a/pkg/ctl/unset/labels.go +++ b/pkg/ctl/unset/labels.go @@ -1,10 +1,10 @@ package unset import ( + "github.com/kris-nova/logger" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/weaveworks/eksctl/pkg/cfn/manager" - "github.com/weaveworks/eksctl/pkg/managed" + "github.com/weaveworks/eksctl/pkg/actions/label" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" @@ -15,7 +15,7 @@ func unsetLabelsCmd(cmd *cmdutils.Cmd) { cfg := api.NewClusterConfig() cmd.ClusterConfig = cfg - cmd.SetDescription("labels", "Create removeLabels", "") + cmd.SetDescription("labels", "Remove labels from managed nodegroups", "") var ( nodeGroupName string @@ -46,6 +46,9 @@ func unsetLabels(cmd *cmdutils.Cmd, nodeGroupName string, removeLabels []string) if cfg.Metadata.Name == "" { return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) } + if nodeGroupName == "" { + return cmdutils.ErrMustBeSet("--nodegroup") + } if cmd.NameArg != "" { return cmdutils.ErrUnsupportedNameArg() @@ -57,7 +60,14 @@ func unsetLabels(cmd *cmdutils.Cmd, nodeGroupName string, removeLabels []string) return err } - stackCollection := manager.NewStackCollection(ctl.Provider, cfg) - managedService := managed.NewService(ctl.Provider, stackCollection, cfg.Metadata.Name) - return managedService.UpdateLabels(nodeGroupName, nil, removeLabels) + cmdutils.LogRegionAndVersionInfo(cmd.ClusterConfig.Metadata) + logger.Info("removing label(s) from nodegroup %s in cluster %s", nodeGroupName, cmd.ClusterConfig.Metadata) + + manager := label.New(cfg, ctl) + if err := manager.Unset(nodeGroupName, removeLabels); err != nil { + return err + } + + logger.Info("done") + return nil } diff --git a/pkg/ctl/unset/unset_test.go b/pkg/ctl/unset/unset_test.go index 3c2ca72cb8..244117d276 100644 --- a/pkg/ctl/unset/unset_test.go +++ b/pkg/ctl/unset/unset_test.go @@ -12,26 +12,42 @@ import ( var _ = Describe("unset", func() { Describe("invalid-resource", func() { - It("with no flag", func() { + It("fails", func() { cmd := newMockCmd("invalid-resource") _, err := cmd.execute() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"unset\"")) Expect(err.Error()).To(ContainSubstring("usage")) }) - It("with invalid-resource and some flag", func() { - cmd := newMockCmd("invalid-resource", "--invalid-flag", "foo") + }) + + Describe("labels", func() { + It("fails when no flags set", func() { + cmd := newMockCmd("labels") _, err := cmd.execute() Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"unset\"")) - Expect(err.Error()).To(ContainSubstring("usage")) + Expect(err.Error()).To(ContainSubstring("Error: required flag(s) \"labels\" not set")) }) - It("with invalid-resource and additional argument", func() { - cmd := newMockCmd("invalid-resource", "foo") + + It("fails when cluster flag not set", func() { + cmd := newMockCmd("labels", "-l", "k") _, err := cmd.execute() Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Error: unknown command \"invalid-resource\" for \"unset\"")) - Expect(err.Error()).To(ContainSubstring("usage")) + Expect(err.Error()).To(ContainSubstring("Error: --cluster must be set")) + }) + + It("fails when --nodegroup flag not set", func() { + cmd := newMockCmd("labels", "--cluster", "dummy", "-l", "k") + _, err := cmd.execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Error: --nodegroup must be set")) + }) + + It("fails when name argument is used", func() { + cmd := newMockCmd("labels", "--cluster", "dummy", "--nodegroup", "dummyNodeGroup", "dummyName", "-l", "k") + _, err := cmd.execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Error: name argument is not supported")) }) }) })