diff --git a/.gitignore b/.gitignore index 5defddd0e9..5295f1e8a2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,8 @@ vendor # Ansible -*.retry \ No newline at end of file +*.retry + +# vim +*~ +*.swp diff --git a/cloud/aws/actuators/machine/actuator.go b/cloud/aws/actuators/machine/actuator.go index 4786d0953f..0d335aa0ed 100644 --- a/cloud/aws/actuators/machine/actuator.go +++ b/cloud/aws/actuators/machine/actuator.go @@ -20,6 +20,7 @@ import ( ec2svc "sigs.k8s.io/cluster-api-provider-aws/cloud/aws/services/ec2" "github.com/golang/glog" + "github.com/pkg/errors" "k8s.io/apimachinery/pkg/runtime" clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" ) @@ -34,6 +35,7 @@ type machinesSvc interface { type ec2Svc interface { CreateInstance(*clusterv1.Machine) (*ec2svc.Instance, error) InstanceIfExists(*string) (*ec2svc.Instance, error) + TerminateInstance(*string) error } // codec are the functions off the generated codec that this actuator uses. @@ -109,7 +111,32 @@ func (a *Actuator) Create(cluster *clusterv1.Cluster, machine *clusterv1.Machine // Delete deletes a machine and is invoked by the Machine Controller func (a *Actuator) Delete(cluster *clusterv1.Cluster, machine *clusterv1.Machine) error { glog.Infof("Deleting machine %v for cluster %v.", machine.Name, cluster.Name) - return fmt.Errorf("TODO: Not yet implemented") + + status, err := a.machineProviderStatus(machine) + if err != nil { + return errors.Wrap(err, "failed to get machine provider status") + } + + instance, err := a.ec2.InstanceIfExists(status.InstanceID) + if err != nil { + return errors.Wrap(err, "failed to get instance") + } + + // Check the instance state. If it's already shutting down or terminated, + // do nothing. Otherwise attempt to delete it. + // This decision is based on the ec2-instance-lifecycle graph at + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html + switch instance.State { + case ec2svc.InstanceStateShuttingDown, ec2svc.InstanceStateTerminated: + return nil + default: + err = a.ec2.TerminateInstance(status.InstanceID) + if err != nil { + return errors.Wrap(err, "failed to terminate instance") + } + } + + return nil } // Update updates a machine and is invoked by the Machine Controller diff --git a/cloud/aws/actuators/machine/actuator_test.go b/cloud/aws/actuators/machine/actuator_test.go index be321aca07..9d749acf54 100644 --- a/cloud/aws/actuators/machine/actuator_test.go +++ b/cloud/aws/actuators/machine/actuator_test.go @@ -14,6 +14,7 @@ package machine_test import ( + "errors" "testing" "sigs.k8s.io/cluster-api-provider-aws/cloud/aws/actuators/machine" @@ -41,6 +42,14 @@ func (e *ec2) InstanceIfExists(id *string) (*ec2svc.Instance, error) { return nil, nil } +func (e *ec2) TerminateInstance(instanceID *string) error { + if instanceID == nil { + return errors.New("didn't receive an instanceID") + } + + return nil +} + type machines struct{} func (m *machines) UpdateMachineStatus(machine *clusterv1.Machine) (*clusterv1.Machine, error) { @@ -66,3 +75,35 @@ func TestCreate(t *testing.T) { t.Fatalf("failed to create machine: %v", err) } } + +func TestDelete(t *testing.T) { + codec, err := v1alpha1.NewCodec() + if err != nil { + t.Fatalf("failed to create a codec: %v", err) + } + + ap := machine.ActuatorParams{ + Codec: codec, + MachinesService: &machines{}, + EC2Service: &ec2{}, + } + + actuator, err := machine.NewActuator(ap) + if err != nil { + t.Fatalf("failed to create an actuator: %v", err) + } + + // Get some empty cluster and machine structs. + testCluster := &clusterv1.Cluster{} + testMachine := &clusterv1.Machine{} + + // Create a machine that we can delete. + if err := actuator.Create(testCluster, testMachine); err != nil { + t.Fatalf("failed to create machine: %v", err) + } + + // Delete the machine. + if err := actuator.Delete(testCluster, testMachine); err != nil { + t.Fatalf("failed to delete machine: %v", err) + } +} diff --git a/cloud/aws/services/ec2/instances.go b/cloud/aws/services/ec2/instances.go index a80de42c6e..8d52f1416f 100644 --- a/cloud/aws/services/ec2/instances.go +++ b/cloud/aws/services/ec2/instances.go @@ -19,6 +19,14 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" ) +const ( + // InstanceStateShuttingDown indicates the instance is shutting-down + InstanceStateShuttingDown = ec2.InstanceStateNameShuttingDown + + // InstanceStateTerminated indicates the instance has been terminated + InstanceStateTerminated = ec2.InstanceStateNameTerminated +) + // Instance is an internal representation of an AWS instance. // This contains more data than the provider config struct tracked in the status. type Instance struct { @@ -67,3 +75,20 @@ func (s *Service) CreateInstance(machine *clusterv1.Machine) (*Instance, error) ID: *reservation.Instances[0].InstanceId, }, nil } + +// TerminateInstance terminates an EC2 instance. +// Returns nil on success, error in all other cases. +func (s *Service) TerminateInstance(instanceID *string) error { + input := &ec2.TerminateInstancesInput{ + InstanceIds: []*string{ + instanceID, + }, + } + + _, err := s.ec2.TerminateInstances(input) + if err != nil { + return err + } + + return nil +} diff --git a/cloud/aws/services/ec2/instances_test.go b/cloud/aws/services/ec2/instances_test.go index fdc490aadf..9e12654c07 100644 --- a/cloud/aws/services/ec2/instances_test.go +++ b/cloud/aws/services/ec2/instances_test.go @@ -19,6 +19,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/golang/mock/gomock" + "github.com/pkg/errors" ec2svc "sigs.k8s.io/cluster-api-provider-aws/cloud/aws/services/ec2" "sigs.k8s.io/cluster-api-provider-aws/cloud/aws/services/ec2/mock_ec2iface" ) @@ -103,3 +104,60 @@ func TestInstanceIfExists(t *testing.T) { }) } } + +func TestTerminateInstance(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + instanceNotFoundError := errors.New("instance not found") + + testCases := []struct { + name string + instanceID string + expect func(m *mock_ec2iface.MockEC2API) + check func(err error) + }{ + { + name: "instance exists", + instanceID: "i-exist", + expect: func(m *mock_ec2iface.MockEC2API) { + m.EXPECT(). + TerminateInstances(gomock.Eq(&ec2.TerminateInstancesInput{ + InstanceIds: []*string{aws.String("i-exist")}, + })). + Return(&ec2.TerminateInstancesOutput{}, nil) + }, + check: func(err error) { + if err != nil { + t.Fatalf("did not expect error: %v", err) + } + }, + }, + { + name: "instance does not exist", + instanceID: "i-donotexist", + expect: func(m *mock_ec2iface.MockEC2API) { + m.EXPECT(). + TerminateInstances(gomock.Eq(&ec2.TerminateInstancesInput{ + InstanceIds: []*string{aws.String("i-donotexist")}, + })). + Return(&ec2.TerminateInstancesOutput{}, instanceNotFoundError) + }, + check: func(err error) { + if err == nil { + t.Fatalf("did not expect error: %v", err) + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ec2Mock := mock_ec2iface.NewMockEC2API(mockCtrl) + tc.expect(ec2Mock) + s := ec2svc.NewService(ec2Mock) + err := s.TerminateInstance(&tc.instanceID) + tc.check(err) + }) + } +}