diff --git a/acceptance/gcp.go b/acceptance/gcp.go index f95a8a3d..18288caf 100644 --- a/acceptance/gcp.go +++ b/acceptance/gcp.go @@ -20,6 +20,7 @@ type GCPAcceptance struct { KeyPath string ProjectId string Zone string + Region string Logger *app.Logger } @@ -43,6 +44,7 @@ func NewGCPAcceptance() GCPAcceptance { KeyPath: path, ProjectId: p.ProjectId, Zone: "us-east1-b", + Region: "us-east1", Logger: app.NewLogger(os.Stdin, os.Stdout, true), } } @@ -55,11 +57,41 @@ func (g GCPAcceptance) InsertDisk(name string) { Expect(err).NotTo(HaveOccurred()) list, err := service.Disks.List(g.ProjectId, g.Zone).Filter(fmt.Sprintf("name eq %s", name)).Do() + Expect(err).NotTo(HaveOccurred()) if len(list.Items) > 0 { return } operation, err := service.Disks.Insert(g.ProjectId, g.Zone, &gcpcompute.Disk{Name: name}).Do() + Expect(err).NotTo(HaveOccurred()) + waiter := compute.NewOperationWaiter(operation, service, g.ProjectId, g.Logger) + + err = waiter.Wait() + Expect(err).NotTo(HaveOccurred()) +} + +func (g GCPAcceptance) InsertCloudRouter(name string) { + config, err := google.JWTConfigFromJSON([]byte(g.Key), gcpcompute.ComputeScope) + Expect(err).NotTo(HaveOccurred()) + + service, err := gcpcompute.New(config.Client(context.Background())) + Expect(err).NotTo(HaveOccurred()) + + list, err := service.Routers.List(g.ProjectId, g.Region).Filter(fmt.Sprintf("name eq %s", name)).Do() + Expect(err).NotTo(HaveOccurred()) + if len(list.Items) > 0 { + return + } + + network, err := service.Networks.Get(g.ProjectId, "default").Do() + Expect(err).NotTo(HaveOccurred()) + + router := &gcpcompute.Router{ + Name: name, + Network: network.SelfLink, + } + operation, err := service.Routers.Insert(g.ProjectId, g.Region, router).Do() + Expect(err).NotTo(HaveOccurred()) waiter := compute.NewOperationWaiter(operation, service, g.ProjectId, g.Logger) diff --git a/acceptance/gcp_test.go b/acceptance/gcp_test.go index 82d423f5..2de1bcc0 100644 --- a/acceptance/gcp_test.go +++ b/acceptance/gcp_test.go @@ -72,6 +72,7 @@ var _ = Describe("GCP", func() { BeforeEach(func() { filter = "leftovers-acceptance" acc.InsertDisk(filter) + acc.InsertCloudRouter(filter) }) It("deletes resources with the filter", func() { @@ -80,6 +81,9 @@ var _ = Describe("GCP", func() { Expect(stdout.String()).To(ContainSubstring("[Disk: leftovers-acceptance] Deleting...")) Expect(stdout.String()).To(ContainSubstring("[Disk: leftovers-acceptance] Deleted!")) + + Expect(stdout.String()).To(ContainSubstring("[Router: leftovers-acceptance] Deleting...")) + Expect(stdout.String()).To(ContainSubstring("[Router: leftovers-acceptance] Deleted!")) }) }) diff --git a/gcp/compute/client.go b/gcp/compute/client.go index 920ae95e..085bfa6e 100644 --- a/gcp/compute/client.go +++ b/gcp/compute/client.go @@ -29,6 +29,7 @@ type client struct { forwardingRules *gcpcompute.ForwardingRulesService globalForwardingRules *gcpcompute.GlobalForwardingRulesService routes *gcpcompute.RoutesService + routers *gcpcompute.RoutersService subnetworks *gcpcompute.SubnetworksService sslCertificates *gcpcompute.SslCertificatesService networks *gcpcompute.NetworksService @@ -63,6 +64,7 @@ func NewClient(project string, service *gcpcompute.Service, logger logger) clien forwardingRules: service.ForwardingRules, globalForwardingRules: service.GlobalForwardingRules, routes: service.Routes, + routers: service.Routers, sslCertificates: service.SslCertificates, subnetworks: service.Subnetworks, networks: service.Networks, @@ -511,6 +513,33 @@ func (c client) DeleteRoute(route string) error { return c.wait(c.routes.Delete(c.project, route)) } +func (c client) ListRouters(region string) ([]*gcpcompute.Router, error) { + var token string + list := []*gcpcompute.Router{} + + for { + resp, err := c.routers.List(c.project, region).PageToken(token).Do() + if err != nil { + return nil, err + } + + list = append(list, resp.Items...) + + token = resp.NextPageToken + if token == "" { + break + } + + time.Sleep(time.Second) + } + + return list, nil +} + +func (c client) DeleteRouter(region, router string) error { + return c.wait(c.routers.Delete(c.project, region, router)) +} + func (c client) ListNetworks() ([]*gcpcompute.Network, error) { var token string list := []*gcpcompute.Network{} diff --git a/gcp/compute/fakes/routers_client.go b/gcp/compute/fakes/routers_client.go new file mode 100644 index 00000000..b8bb13d8 --- /dev/null +++ b/gcp/compute/fakes/routers_client.go @@ -0,0 +1,42 @@ +package fakes + +import gcpcompute "google.golang.org/api/compute/v1" + +type RoutersClient struct { + ListRoutersCall struct { + CallCount int + Receives struct { + Region string + } + Returns struct { + Output []*gcpcompute.Router + Error error + } + } + + DeleteRouterCall struct { + CallCount int + Receives struct { + Router string + Region string + } + Returns struct { + Error error + } + } +} + +func (n *RoutersClient) ListRouters(region string) ([]*gcpcompute.Router, error) { + n.ListRoutersCall.CallCount++ + n.ListRoutersCall.Receives.Region = region + + return n.ListRoutersCall.Returns.Output, n.ListRoutersCall.Returns.Error +} + +func (n *RoutersClient) DeleteRouter(region, router string) error { + n.DeleteRouterCall.CallCount++ + n.DeleteRouterCall.Receives.Region = region + n.DeleteRouterCall.Receives.Router = router + + return n.DeleteRouterCall.Returns.Error +} diff --git a/gcp/compute/router.go b/gcp/compute/router.go new file mode 100644 index 00000000..147b5ec4 --- /dev/null +++ b/gcp/compute/router.go @@ -0,0 +1,35 @@ +package compute + +import "fmt" + +type Router struct { + client routersClient + name string + region string +} + +func NewRouter(client routersClient, name, region string) Router { + return Router{ + client: client, + name: name, + region: region, + } +} + +func (r Router) Delete() error { + err := r.client.DeleteRouter(r.region, r.name) + + if err != nil { + return fmt.Errorf("Delete: %s", err) + } + + return nil +} + +func (r Router) Type() string { + return "Router" +} + +func (r Router) Name() string { + return r.name +} diff --git a/gcp/compute/router_test.go b/gcp/compute/router_test.go new file mode 100644 index 00000000..2e56a12d --- /dev/null +++ b/gcp/compute/router_test.go @@ -0,0 +1,63 @@ +package compute_test + +import ( + "errors" + + "github.com/genevieve/leftovers/gcp/compute" + "github.com/genevieve/leftovers/gcp/compute/fakes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Router", func() { + var ( + client *fakes.RoutersClient + name string + region string + + router compute.Router + ) + + BeforeEach(func() { + client = &fakes.RoutersClient{} + name = "banana" + region = "region-1" + + router = compute.NewRouter(client, name, region) + }) + + Describe("Delete", func() { + It("deletes the router", func() { + err := router.Delete() + Expect(err).NotTo(HaveOccurred()) + + Expect(client.DeleteRouterCall.CallCount).To(Equal(1)) + Expect(client.DeleteRouterCall.Receives.Region).To(Equal(region)) + Expect(client.DeleteRouterCall.Receives.Router).To(Equal(name)) + }) + + Context("when the client fails to delete", func() { + BeforeEach(func() { + client.DeleteRouterCall.Returns.Error = errors.New("the-error") + }) + + It("returns the error", func() { + err := router.Delete() + Expect(err).To(MatchError("Delete: the-error")) + }) + }) + }) + + Describe("Name", func() { + It("returns the name", func() { + Expect(router.Name()).To(Equal(name)) + }) + }) + + Describe("Type", func() { + It("returns the type", func() { + Expect(router.Type()).To(Equal("Router")) + }) + }) +}) diff --git a/gcp/compute/routers.go b/gcp/compute/routers.go new file mode 100644 index 00000000..8c384fe6 --- /dev/null +++ b/gcp/compute/routers.go @@ -0,0 +1,62 @@ +package compute + +import ( + "fmt" + "strings" + + "github.com/genevieve/leftovers/common" + gcpcompute "google.golang.org/api/compute/v1" +) + +type routersClient interface { + ListRouters(region string) ([]*gcpcompute.Router, error) + DeleteRouter(region, router string) error +} + +type Routers struct { + routersClient routersClient + logger logger + regions map[string]string +} + +func NewRouters(routersClient routersClient, logger logger, regions map[string]string) Routers { + return Routers{ + routersClient: routersClient, + logger: logger, + regions: regions, + } +} + +func (r Routers) List(filter string) ([]common.Deletable, error) { + routers := []*gcpcompute.Router{} + for _, region := range r.regions { + l, err := r.routersClient.ListRouters(region) + if err != nil { + return []common.Deletable{}, fmt.Errorf("List Routers for region %s: %s", region, err) + } + + routers = append(routers, l...) + } + + var resources []common.Deletable + for _, router := range routers { + resource := NewRouter(r.routersClient, router.Name, r.regions[router.Region]) + + if !strings.Contains(resource.Name(), filter) { + continue + } + + proceed := r.logger.PromptWithDetails(resource.Type(), resource.Name()) + if !proceed { + continue + } + + resources = append(resources, resource) + } + + return resources, nil +} + +func (r Routers) Type() string { + return "cloud-router" +} diff --git a/gcp/compute/routers_test.go b/gcp/compute/routers_test.go new file mode 100644 index 00000000..60614f49 --- /dev/null +++ b/gcp/compute/routers_test.go @@ -0,0 +1,83 @@ +package compute_test + +import ( + "errors" + + "github.com/genevieve/leftovers/gcp/compute" + "github.com/genevieve/leftovers/gcp/compute/fakes" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + gcpcompute "google.golang.org/api/compute/v1" +) + +var _ = Describe("Routers", func() { + var ( + client *fakes.RoutersClient + logger *fakes.Logger + regions map[string]string + + routers compute.Routers + ) + + BeforeEach(func() { + client = &fakes.RoutersClient{} + logger = &fakes.Logger{} + regions = map[string]string{"https://region-1": "region-1"} + + routers = compute.NewRouters(client, logger, regions) + }) + + Describe("List", func() { + var filter string + + BeforeEach(func() { + logger.PromptWithDetailsCall.Returns.Proceed = true + client.ListRoutersCall.Returns.Output = []*gcpcompute.Router{ + { + Name: "banana-router", + Region: "https://region-1", + }, + { + Name: "pineapple-router", + Region: "https://region-1", + }, + } + filter = "banana" + }) + + It("lists, filters, and prompts for routers to delete", func() { + list, err := routers.List(filter) + Expect(err).NotTo(HaveOccurred()) + + Expect(client.ListRoutersCall.CallCount).To(Equal(1)) + Expect(logger.PromptWithDetailsCall.CallCount).To(Equal(1)) + Expect(logger.PromptWithDetailsCall.Receives.Name).To(Equal("banana-router")) + Expect(logger.PromptWithDetailsCall.Receives.Type).To(Equal("Router")) + + Expect(list).To(HaveLen(1)) + }) + + Context("when routers client fails to list routers", func() { + BeforeEach(func() { + client.ListRoutersCall.Returns.Error = errors.New("some error") + }) + + It("returns helpful error message", func() { + _, err := routers.List(filter) + Expect(err).To(MatchError("List Routers for region region-1: some error")) + }) + }) + + Context("when the user does not want to delete resource", func() { + BeforeEach(func() { + logger.PromptWithDetailsCall.Returns.Proceed = false + }) + + It("removes it from the list", func() { + list, err := routers.List(filter) + Expect(err).NotTo(HaveOccurred()) + Expect(list).To(HaveLen(0)) + }) + }) + }) +}) diff --git a/gcp/leftovers.go b/gcp/leftovers.go index 36116d24..06f96277 100644 --- a/gcp/leftovers.go +++ b/gcp/leftovers.go @@ -149,6 +149,7 @@ func NewLeftovers(logger logger, keyPath string) (Leftovers, error) { compute.NewVpnTunnels(client, logger, regions), compute.NewTargetVpnGateways(client, logger, regions), compute.NewRoutes(client, logger), + compute.NewRouters(client, logger, regions), compute.NewSubnetworks(client, logger, regions), compute.NewGlobalAddresses(client, logger), compute.NewNetworks(client, logger),