diff --git a/acceptance/azure_test.go b/acceptance/azure_test.go index 9189b2b8..e81dd0e8 100644 --- a/acceptance/azure_test.go +++ b/acceptance/azure_test.go @@ -80,5 +80,24 @@ var _ = Describe("Azure", func() { Expect(stdout.String()).To(ContainSubstring("[Resource Group: %s] Deleting...", filter)) Expect(stdout.String()).To(ContainSubstring("[Resource Group: %s] Deleted!", filter)) }) + + Context("when the user wants to delete subresources of the resource group", func() { + BeforeEach(func() { + filter = "leftovers-acc-sub-delete" + acc.CreateResourceGroup(filter) + // acc.CreateAppSecurityGroup(filter) + }) + + PIt("prompts them for subresources after they say no to the resource group", func() { + err := deleter.Delete(filter) + Expect(err).NotTo(HaveOccurred()) + + Expect(stdout.String()).NotTo(ContainSubstring("[Resource Group: %s] Deleting...", filter)) + Expect(stdout.String()).NotTo(ContainSubstring("[Resource Group: %s] Deleted!", filter)) + + Expect(stdout.String()).To(ContainSubstring("[Application Security Group: %s] Deleting...", filter)) + Expect(stdout.String()).To(ContainSubstring("[Application Security Group: %s] Deleted!", filter)) + }) + }) }) }) diff --git a/azure/app_security_group.go b/azure/app_security_group.go new file mode 100644 index 00000000..c92823c1 --- /dev/null +++ b/azure/app_security_group.go @@ -0,0 +1,36 @@ +package azure + +import "fmt" + +type AppSecurityGroup struct { + client appSecurityGroupsClient + name string + rgName string +} + +// AppSecurityGroup represents an Azure application security group. +func NewAppSecurityGroup(client appSecurityGroupsClient, rgName, name string) AppSecurityGroup { + return AppSecurityGroup{ + client: client, + rgName: rgName, + name: name, + } +} + +// Delete deletes an Azure application security group. +func (g AppSecurityGroup) Delete() error { + err := g.client.DeleteAppSecurityGroup(g.rgName, g.name) + if err != nil { + return fmt.Errorf("Delete: %s", err) + } + + return nil +} + +func (g AppSecurityGroup) Name() string { + return g.name +} + +func (g AppSecurityGroup) Type() string { + return "Application Security Group" +} diff --git a/azure/app_security_group_test.go b/azure/app_security_group_test.go new file mode 100644 index 00000000..b1e098d0 --- /dev/null +++ b/azure/app_security_group_test.go @@ -0,0 +1,56 @@ +package azure_test + +import ( + "errors" + + "github.com/genevieve/leftovers/azure" + "github.com/genevieve/leftovers/azure/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppSecurityGroup", func() { + var ( + client *fakes.AppSecurityGroupsClient + name string + rgName string + + group azure.AppSecurityGroup + ) + + BeforeEach(func() { + client = &fakes.AppSecurityGroupsClient{} + name = "banana-group" + rgName = "major-banana-group" + + group = azure.NewAppSecurityGroup(client, rgName, name) + }) + + Describe("Delete", func() { + It("deletes resource groups", func() { + err := group.Delete() + Expect(err).NotTo(HaveOccurred()) + + Expect(client.DeleteAppSecurityGroupCall.CallCount).To(Equal(1)) + Expect(client.DeleteAppSecurityGroupCall.Receives.Name).To(Equal(name)) + Expect(client.DeleteAppSecurityGroupCall.Receives.ResourceGroupName).To(Equal(rgName)) + }) + + Context("when client fails to delete the app security group", func() { + BeforeEach(func() { + client.DeleteAppSecurityGroupCall.Returns.Error = errors.New("some error") + }) + + It("logs the error", func() { + err := group.Delete() + Expect(err).To(MatchError("Delete: some error")) + }) + }) + }) + + Describe("Type", func() { + It("returns the type", func() { + Expect(group.Type()).To(Equal("Application Security Group")) + }) + }) +}) diff --git a/azure/app_security_groups.go b/azure/app_security_groups.go new file mode 100644 index 00000000..de80a6ff --- /dev/null +++ b/azure/app_security_groups.go @@ -0,0 +1,56 @@ +package azure + +import ( + "fmt" + "strings" + + "github.com/genevieve/leftovers/common" +) + +type appSecurityGroupsClient interface { + ListAppSecurityGroups(rgName string) ([]string, error) + DeleteAppSecurityGroup(rgName string, name string) error +} + +type AppSecurityGroups struct { + client appSecurityGroupsClient + rgName string + logger logger +} + +func NewAppSecurityGroups(client appSecurityGroupsClient, rgName string, logger logger) AppSecurityGroups { + return AppSecurityGroups{ + client: client, + rgName: rgName, + logger: logger, + } +} + +func (g AppSecurityGroups) List(filter string) ([]common.Deletable, error) { + groups, err := g.client.ListAppSecurityGroups(g.rgName) + if err != nil { + return nil, fmt.Errorf("Listing Application Security Groups: %s", err) + } + + var resources []common.Deletable + for _, group := range groups { + r := NewAppSecurityGroup(g.client, g.rgName, group) + + if !strings.Contains(r.Name(), filter) { + continue + } + + proceed := g.logger.PromptWithDetails(r.Type(), r.Name()) + if !proceed { + continue + } + + resources = append(resources, r) + } + + return resources, nil +} + +func (g AppSecurityGroups) Type() string { + return "app-security-group" +} diff --git a/azure/app_security_groups_test.go b/azure/app_security_groups_test.go new file mode 100644 index 00000000..cd08012d --- /dev/null +++ b/azure/app_security_groups_test.go @@ -0,0 +1,86 @@ +package azure_test + +import ( + "errors" + + "github.com/genevieve/leftovers/azure" + "github.com/genevieve/leftovers/azure/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("AppSecurityGroups", func() { + var ( + client *fakes.AppSecurityGroupsClient + logger *fakes.Logger + filter string + rgName string + + groups azure.AppSecurityGroups + ) + + BeforeEach(func() { + client = &fakes.AppSecurityGroupsClient{} + logger = &fakes.Logger{} + filter = "banana" + rgName = "resource-group" + + groups = azure.NewAppSecurityGroups(client, rgName, logger) + }) + + Describe("List", func() { + BeforeEach(func() { + logger.PromptWithDetailsCall.Returns.Proceed = true + client.ListAppSecurityGroupsCall.Returns.List = []string{"banana-group", "kiwi-group"} + }) + + It("returns a list of resources to delete", func() { + items, err := groups.List(filter) + Expect(err).NotTo(HaveOccurred()) + + Expect(client.ListAppSecurityGroupsCall.CallCount).To(Equal(1)) + Expect(client.ListAppSecurityGroupsCall.Receives.ResourceGroupName).To(Equal(rgName)) + + Expect(logger.PromptWithDetailsCall.CallCount).To(Equal(1)) + + Expect(items).To(HaveLen(1)) + }) + + Context("when client fails to list the resource", func() { + BeforeEach(func() { + client.ListAppSecurityGroupsCall.Returns.Error = errors.New("some error") + }) + + It("returns the error", func() { + _, err := groups.List(filter) + Expect(err).To(MatchError("Listing Application Security Groups: some error")) + }) + }) + + Context("when the user responds no to the prompt", func() { + BeforeEach(func() { + logger.PromptWithDetailsCall.Returns.Proceed = false + }) + + It("does not return it in the list", func() { + items, err := groups.List(filter) + Expect(err).NotTo(HaveOccurred()) + + Expect(logger.PromptWithDetailsCall.Receives.Type).To(Equal("Application Security Group")) + Expect(logger.PromptWithDetailsCall.Receives.Name).To(Equal("banana-group")) + + Expect(items).To(HaveLen(0)) + }) + }) + + Context("when the resource group name does not contain the filter", func() { + It("does not return it in the list", func() { + items, err := groups.List("grape") + Expect(err).NotTo(HaveOccurred()) + + Expect(logger.PromptWithDetailsCall.CallCount).To(Equal(0)) + Expect(items).To(HaveLen(0)) + }) + }) + }) +}) diff --git a/azure/client.go b/azure/client.go index 1bc9b29d..598cad89 100644 --- a/azure/client.go +++ b/azure/client.go @@ -4,19 +4,22 @@ import ( "context" "fmt" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-04-01/network" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources" "github.com/Azure/go-autorest/autorest" ) type client struct { - groupsClient resources.GroupsClient + rgClient resources.GroupsClient + sgClient network.ApplicationSecurityGroupsClient autorestClient autorest.Client } -func NewClient(groupsClient resources.GroupsClient) client { +func NewClient(rg resources.GroupsClient, sg network.ApplicationSecurityGroupsClient) client { return client{ - groupsClient: groupsClient, - autorestClient: groupsClient.Client, + rgClient: rg, + sgClient: sg, + autorestClient: rg.Client, } } @@ -24,9 +27,9 @@ func (c client) ListGroups() ([]string, error) { ctx := context.Background() groups := []string{} - for list, err := c.groupsClient.ListComplete(ctx, "", nil); list.NotDone(); err = list.Next() { + for list, err := c.rgClient.ListComplete(ctx, "", nil); list.NotDone(); err = list.Next() { if err != nil { - return []string{}, fmt.Errorf("List Groups: %s", err) + return []string{}, fmt.Errorf("List Complete Resource Groups: %s", err) } groups = append(groups, *list.Value().Name) @@ -38,7 +41,38 @@ func (c client) ListGroups() ([]string, error) { func (c client) DeleteGroup(name string) error { ctx := context.Background() - future, err := c.groupsClient.Delete(ctx, name) + future, err := c.rgClient.Delete(ctx, name) + if err != nil { + return err + } + + err = future.WaitForCompletionRef(ctx, c.autorestClient) + if err != nil { + return fmt.Errorf("Waiting for completion: %s", err) + } + + return nil +} + +func (c client) ListAppSecurityGroups(rgName string) ([]string, error) { + ctx := context.Background() + groups := []string{} + + for list, err := c.sgClient.ListComplete(ctx, rgName); list.NotDone(); err = list.Next() { + if err != nil { + return groups, fmt.Errorf("List Complete App Security Groups: %s", err) + } + + groups = append(groups, *list.Value().Name) + } + + return groups, nil +} + +func (c client) DeleteAppSecurityGroup(rgName, name string) error { + ctx := context.Background() + + future, err := c.sgClient.Delete(ctx, rgName, name) if err != nil { return err } diff --git a/azure/fakes/app_security_groups_client.go b/azure/fakes/app_security_groups_client.go new file mode 100644 index 00000000..0771d0e3 --- /dev/null +++ b/azure/fakes/app_security_groups_client.go @@ -0,0 +1,39 @@ +package fakes + +type AppSecurityGroupsClient struct { + ListAppSecurityGroupsCall struct { + CallCount int + Receives struct { + ResourceGroupName string + } + Returns struct { + List []string + Error error + } + } + + DeleteAppSecurityGroupCall struct { + CallCount int + Receives struct { + ResourceGroupName string + Name string + } + Returns struct { + Error error + } + } +} + +func (c *AppSecurityGroupsClient) ListAppSecurityGroups(rgName string) ([]string, error) { + c.ListAppSecurityGroupsCall.CallCount++ + c.ListAppSecurityGroupsCall.Receives.ResourceGroupName = rgName + return c.ListAppSecurityGroupsCall.Returns.List, c.ListAppSecurityGroupsCall.Returns.Error +} + +func (c *AppSecurityGroupsClient) DeleteAppSecurityGroup(rgName, name string) error { + c.DeleteAppSecurityGroupCall.CallCount++ + c.DeleteAppSecurityGroupCall.Receives.ResourceGroupName = rgName + c.DeleteAppSecurityGroupCall.Receives.Name = name + + return c.DeleteAppSecurityGroupCall.Returns.Error +} diff --git a/azure/leftovers.go b/azure/leftovers.go index 3a2611de..ea3891e7 100644 --- a/azure/leftovers.go +++ b/azure/leftovers.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-04-01/network" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" @@ -58,6 +59,7 @@ func (l Leftovers) Delete(filter string) error { result *multierror.Error ) + // TODO: If they say no to the Resource Group, prompt for individual resources. deletables, err := l.resource.List(filter) if err != nil { l.logger.Println(color.YellowString(err.Error())) @@ -113,15 +115,18 @@ func NewLeftovers(logger logger, clientId, clientSecret, subscriptionId, tenantI return Leftovers{}, fmt.Errorf("Creating oauth config: %s\n", err) } - servicePrincipalToken, err := adal.NewServicePrincipalToken(*oauthConfig, clientId, clientSecret, azurelib.PublicCloud.ResourceManagerEndpoint) + token, err := adal.NewServicePrincipalToken(*oauthConfig, clientId, clientSecret, azurelib.PublicCloud.ResourceManagerEndpoint) if err != nil { return Leftovers{}, fmt.Errorf("Creating service principal token: %s\n", err) } - gc := resources.NewGroupsClient(subscriptionId) - gc.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken) + rg := resources.NewGroupsClient(subscriptionId) + rg.Authorizer = autorest.NewBearerAuthorizer(token) - client := NewClient(gc) + sg := network.NewApplicationSecurityGroupsClient(subscriptionId) + sg.Authorizer = autorest.NewBearerAuthorizer(token) + + client := NewClient(rg, sg) return Leftovers{ logger: logger,