diff --git a/pkg/cloud/aws/actuators/machine/actuator_test.go b/pkg/cloud/aws/actuators/machine/actuator_test.go index 7b9644caf4..4825d5d24e 100644 --- a/pkg/cloud/aws/actuators/machine/actuator_test.go +++ b/pkg/cloud/aws/actuators/machine/actuator_test.go @@ -15,10 +15,6 @@ import ( kubernetesfake "k8s.io/client-go/kubernetes/fake" - apiv1 "k8s.io/api/core/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" providerconfigv1 "sigs.k8s.io/cluster-api-provider-aws/pkg/apis/awsproviderconfig/v1alpha1" @@ -29,7 +25,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/cluster-api-provider-aws/test/utils" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -39,129 +34,6 @@ func init() { clusterv1.AddToScheme(scheme.Scheme) } -const ( - controllerLogName = "awsMachine" - - defaultNamespace = "default" - defaultAvailabilityZone = "us-east-1a" - region = "us-east-1" - awsCredentialsSecretName = "aws-credentials-secret" - userDataSecretName = "aws-actuator-user-data-secret" - - keyName = "aws-actuator-key-name" - clusterID = "aws-actuator-cluster" -) - -const userDataBlob = `#cloud-config -write_files: -- path: /root/node_bootstrap/node_settings.yaml - owner: 'root:root' - permissions: '0640' - content: | - node_config_name: node-config-master -runcmd: -- [ cat, /root/node_bootstrap/node_settings.yaml] -` - -func testMachineAPIResources(clusterID string) (*clusterv1.Machine, *clusterv1.Cluster, *apiv1.Secret, *apiv1.Secret, error) { - awsCredentialsSecret := utils.GenerateAwsCredentialsSecretFromEnv(awsCredentialsSecretName, defaultNamespace) - - userDataSecret := &apiv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: userDataSecretName, - Namespace: defaultNamespace, - }, - Data: map[string][]byte{ - userDataSecretKey: []byte(userDataBlob), - }, - } - - machinePc := &providerconfigv1.AWSMachineProviderConfig{ - AMI: providerconfigv1.AWSResourceReference{ - ID: aws.String("ami-a9acbbd6"), - }, - CredentialsSecret: &corev1.LocalObjectReference{ - Name: awsCredentialsSecretName, - }, - InstanceType: "m4.xlarge", - Placement: providerconfigv1.Placement{ - Region: region, - AvailabilityZone: defaultAvailabilityZone, - }, - Subnet: providerconfigv1.AWSResourceReference{ - ID: aws.String("subnet-0e56b13a64ff8a941"), - }, - IAMInstanceProfile: &providerconfigv1.AWSResourceReference{ - ID: aws.String("openshift_master_launch_instances"), - }, - KeyName: aws.String(keyName), - UserDataSecret: &corev1.LocalObjectReference{ - Name: userDataSecretName, - }, - Tags: []providerconfigv1.TagSpecification{ - {Name: "openshift-node-group-config", Value: "node-config-master"}, - {Name: "host-type", Value: "master"}, - {Name: "sub-host-type", Value: "default"}, - }, - SecurityGroups: []providerconfigv1.AWSResourceReference{ - {ID: aws.String("sg-00868b02fbe29de17")}, // aws-actuator - {ID: aws.String("sg-0a4658991dc5eb40a")}, // aws-actuator_master - {ID: aws.String("sg-009a70e28fa4ba84e")}, // aws-actuator_master_k8s - {ID: aws.String("sg-07323d56fb932c84c")}, // aws-actuator_infra - {ID: aws.String("sg-08b1ffd32874d59a2")}, // aws-actuator_infra_k8s - }, - PublicIP: aws.Bool(true), - LoadBalancers: []providerconfigv1.LoadBalancerReference{ - { - Name: "cluster-con", - Type: providerconfigv1.ClassicLoadBalancerType, - }, - { - Name: "cluster-ext", - Type: providerconfigv1.ClassicLoadBalancerType, - }, - { - Name: "cluster-int", - Type: providerconfigv1.ClassicLoadBalancerType, - }, - }, - } - - codec, err := providerconfigv1.NewCodec() - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed creating codec: %v", err) - } - config, err := codec.EncodeProviderConfig(machinePc) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("encodeToProviderConfig failed: %v", err) - } - - machine := &clusterv1.Machine{ - ObjectMeta: metav1.ObjectMeta{ - Name: "aws-actuator-testing-machine", - Namespace: defaultNamespace, - Labels: map[string]string{ - providerconfigv1.ClusterIDLabel: clusterID, - providerconfigv1.MachineRoleLabel: "infra", - providerconfigv1.MachineTypeLabel: "master", - }, - }, - - Spec: clusterv1.MachineSpec{ - ProviderConfig: *config, - }, - } - - cluster := &clusterv1.Cluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterID, - Namespace: defaultNamespace, - }, - } - - return machine, cluster, awsCredentialsSecret, userDataSecret, nil -} - func TestCreateAndDeleteMachine(t *testing.T) { cases := []struct { name string @@ -187,7 +59,7 @@ func TestCreateAndDeleteMachine(t *testing.T) { // - kubeClient.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{}) // cluster client for updating machine statues // - clusterClient.ClusterV1alpha1().Machines(machineCopy.Namespace).UpdateStatus(machineCopy) - machine, cluster, awsCredentialsSecret, userDataSecret, err := testMachineAPIResources(clusterID) + machine, cluster, awsCredentialsSecret, userDataSecret, err := stubMachineAPIResources() if err != nil { t.Fatal(err) } @@ -376,7 +248,7 @@ func TestAvailabiltyZone(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - machine, cluster, awsCredentialsSecret, userDataSecret, err := testMachineAPIResources(clusterID) + machine, cluster, awsCredentialsSecret, userDataSecret, err := stubMachineAPIResources() if err != nil { t.Fatal(err) } diff --git a/pkg/cloud/aws/actuators/machine/instaces_test.go b/pkg/cloud/aws/actuators/machine/instaces_test.go index acf3343a20..3815e22428 100644 --- a/pkg/cloud/aws/actuators/machine/instaces_test.go +++ b/pkg/cloud/aws/actuators/machine/instaces_test.go @@ -1,12 +1,17 @@ package machine import ( + "fmt" "reflect" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" providerconfigv1 "sigs.k8s.io/cluster-api-provider-aws/pkg/apis/awsproviderconfig/v1alpha1" + + "github.com/golang/mock/gomock" + mockaws "sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/aws/client/mock" ) func TestRemoveDuplicatedTags(t *testing.T) { @@ -84,3 +89,272 @@ func TestBuildEC2Filters(t *testing.T) { t.Errorf("failed to buildEC2Filters. Expected: %+v, got: %+v", expected, got) } } + +func TestRemoveStoppedMachine(t *testing.T) { + machine, _, err := stubMachine() + if err != nil { + t.Errorf("Unable to build test machine manifest: %v", err) + } + + cases := []struct { + name string + output *ec2.DescribeInstancesOutput + err error + }{ + { + name: "DescribeInstances with error", + output: &ec2.DescribeInstancesOutput{}, + // any non-nil error will do + err: fmt.Errorf("error describing instances"), + }, + { + name: "No instances to stop", + output: &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{}, + }, + }, + }, + }, + { + name: "One instance to stop", + output: &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + stubInstance("ami-a9acbbd6", "i-02fcb933c5da7085c"), + }, + }, + }, + }, + }, + { + name: "Two instances to stop", + output: &ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + stubInstance("ami-a9acbbd6", "i-02fcb933c5da7085c"), + stubInstance("ami-a9acbbd7", "i-02fcb933c5da7085d"), + }, + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockAWSClient := mockaws.NewMockClient(mockCtrl) + // Not here to check how many times all the mocked methods get called. + // Rather to provide fake outputs to get through all possible execution paths. + mockAWSClient.EXPECT().DescribeInstances(gomock.Any()).Return(tc.output, tc.err).AnyTimes() + mockAWSClient.EXPECT().TerminateInstances(gomock.Any()).AnyTimes() + removeStoppedMachine(machine, mockAWSClient) + }) + } +} + +func TestRunningInstace(t *testing.T) { + machine, _, err := stubMachine() + if err != nil { + t.Fatalf("Unable to build test machine manifest: %v", err) + } + + mockCtrl := gomock.NewController(t) + mockAWSClient := mockaws.NewMockClient(mockCtrl) + + // Error describing instances + mockAWSClient.EXPECT().DescribeInstances(gomock.Any()).Return(&ec2.DescribeInstancesOutput{ + Reservations: []*ec2.Reservation{ + { + Instances: []*ec2.Instance{ + { + ImageId: aws.String("ami-a9acbbd6"), + InstanceId: aws.String("i-02fcb933c5da7085c"), + State: &ec2.InstanceState{ + Name: aws.String("Running"), + }, + LaunchTime: aws.Time(time.Now()), + }, + }, + }, + }, + }, nil).AnyTimes() + getRunningInstance(machine, mockAWSClient) +} + +func TestLaunchInstance(t *testing.T) { + machine, _, err := stubMachine() + if err != nil { + t.Fatalf("Unable to build test machine manifest: %v", err) + } + + cases := []struct { + name string + providerConfig *providerconfigv1.AWSMachineProviderConfig + securityGroupOutput *ec2.DescribeSecurityGroupsOutput + securityGroupErr error + subnetOutput *ec2.DescribeSubnetsOutput + subnetErr error + imageOutput *ec2.DescribeImagesOutput + imageErr error + }{ + { + name: "Security groups with filters", + providerConfig: stubPCSecurityGroups( + []providerconfigv1.AWSResourceReference{ + { + Filters: []providerconfigv1.Filter{}, + }, + }, + ), + securityGroupOutput: &ec2.DescribeSecurityGroupsOutput{ + SecurityGroups: []*ec2.SecurityGroup{ + { + GroupId: aws.String("groupID"), + }, + }, + }, + }, + { + name: "Security groups with filters with error", + providerConfig: stubPCSecurityGroups( + []providerconfigv1.AWSResourceReference{ + { + Filters: []providerconfigv1.Filter{}, + }, + }, + ), + securityGroupErr: fmt.Errorf("error"), + }, + { + name: "No security group", + providerConfig: stubPCSecurityGroups( + []providerconfigv1.AWSResourceReference{ + { + Filters: []providerconfigv1.Filter{}, + }, + }, + ), + securityGroupOutput: &ec2.DescribeSecurityGroupsOutput{ + SecurityGroups: []*ec2.SecurityGroup{}, + }, + }, + { + name: "Subnet with filters", + providerConfig: stubPCSubnet(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{}, + }), + subnetOutput: &ec2.DescribeSubnetsOutput{ + Subnets: []*ec2.Subnet{ + { + SubnetId: aws.String("subnetID"), + }, + }, + }, + }, + { + name: "Subnet with filters with error", + providerConfig: stubPCSubnet(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{}, + }), + subnetErr: fmt.Errorf("error"), + }, + { + name: "AMI with filters", + providerConfig: stubPCAMI(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{}, + }), + imageOutput: &ec2.DescribeImagesOutput{ + Images: []*ec2.Image{ + { + CreationDate: aws.String(time.RFC3339), + ImageId: aws.String("ami-1111"), + }, + }, + }, + }, + { + name: "AMI with filters with error", + providerConfig: stubPCAMI(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{}, + }), + imageErr: fmt.Errorf("error"), + }, + { + name: "AMI with filters with no image", + providerConfig: stubPCAMI(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{ + { + Name: "image_stage", + Values: []string{"base"}, + }, + }, + }), + imageOutput: &ec2.DescribeImagesOutput{ + Images: []*ec2.Image{}, + }, + }, + { + name: "AMI with filters with two images", + providerConfig: stubPCAMI(providerconfigv1.AWSResourceReference{ + Filters: []providerconfigv1.Filter{ + { + Name: "image_stage", + Values: []string{"base"}, + }, + }, + }), + imageOutput: &ec2.DescribeImagesOutput{ + Images: []*ec2.Image{ + { + CreationDate: aws.String("2006-01-02T15:04:05Z"), + ImageId: aws.String("ami-1111"), + }, + { + CreationDate: aws.String("2006-01-02T15:04:05Z"), + ImageId: aws.String("ami-2222"), + }, + }, + }, + }, + { + name: "AMI not specified", + providerConfig: stubPCAMI(providerconfigv1.AWSResourceReference{}), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + mockAWSClient := mockaws.NewMockClient(mockCtrl) + + mockAWSClient.EXPECT().DescribeSecurityGroups(gomock.Any()).Return(tc.securityGroupOutput, tc.securityGroupErr).AnyTimes() + mockAWSClient.EXPECT().DescribeSubnets(gomock.Any()).Return(tc.subnetOutput, tc.subnetErr).AnyTimes() + mockAWSClient.EXPECT().DescribeImages(gomock.Any()).Return(tc.imageOutput, tc.imageErr).AnyTimes() + mockAWSClient.EXPECT().RunInstances(gomock.Any()) + + launchInstance(machine, tc.providerConfig, nil, mockAWSClient) + }) + } +} + +func TestSortInstances(t *testing.T) { + instances := []*ec2.Instance{ + { + LaunchTime: aws.Time(time.Now()), + }, + { + LaunchTime: nil, + }, + { + LaunchTime: nil, + }, + { + LaunchTime: aws.Time(time.Now()), + }, + } + sortInstances(instances) +} diff --git a/pkg/cloud/aws/actuators/machine/stubs.go b/pkg/cloud/aws/actuators/machine/stubs.go new file mode 100644 index 0000000000..15c786bb00 --- /dev/null +++ b/pkg/cloud/aws/actuators/machine/stubs.go @@ -0,0 +1,201 @@ +package machine + +import ( + "fmt" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + apiv1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + providerconfigv1 "sigs.k8s.io/cluster-api-provider-aws/pkg/apis/awsproviderconfig/v1alpha1" + "sigs.k8s.io/cluster-api-provider-aws/test/utils" + clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" +) + +const ( + defaultNamespace = "default" + defaultAvailabilityZone = "us-east-1a" + region = "us-east-1" + awsCredentialsSecretName = "aws-credentials-secret" + userDataSecretName = "aws-actuator-user-data-secret" + + keyName = "aws-actuator-key-name" + clusterID = "aws-actuator-cluster" +) + +const userDataBlob = `#cloud-config +write_files: +- path: /root/node_bootstrap/node_settings.yaml + owner: 'root:root' + permissions: '0640' + content: | + node_config_name: node-config-master +runcmd: +- [ cat, /root/node_bootstrap/node_settings.yaml] +` + +func stubProviderConfig() *providerconfigv1.AWSMachineProviderConfig { + return &providerconfigv1.AWSMachineProviderConfig{ + AMI: providerconfigv1.AWSResourceReference{ + ID: aws.String("ami-a9acbbd6"), + }, + CredentialsSecret: &corev1.LocalObjectReference{ + Name: awsCredentialsSecretName, + }, + InstanceType: "m4.xlarge", + Placement: providerconfigv1.Placement{ + Region: region, + AvailabilityZone: defaultAvailabilityZone, + }, + Subnet: providerconfigv1.AWSResourceReference{ + ID: aws.String("subnet-0e56b13a64ff8a941"), + }, + IAMInstanceProfile: &providerconfigv1.AWSResourceReference{ + ID: aws.String("openshift_master_launch_instances"), + }, + KeyName: aws.String(keyName), + UserDataSecret: &corev1.LocalObjectReference{ + Name: userDataSecretName, + }, + Tags: []providerconfigv1.TagSpecification{ + {Name: "openshift-node-group-config", Value: "node-config-master"}, + {Name: "host-type", Value: "master"}, + {Name: "sub-host-type", Value: "default"}, + }, + SecurityGroups: []providerconfigv1.AWSResourceReference{ + {ID: aws.String("sg-00868b02fbe29de17")}, + {ID: aws.String("sg-0a4658991dc5eb40a")}, + {ID: aws.String("sg-009a70e28fa4ba84e")}, + {ID: aws.String("sg-07323d56fb932c84c")}, + {ID: aws.String("sg-08b1ffd32874d59a2")}, + }, + PublicIP: aws.Bool(true), + LoadBalancers: []providerconfigv1.LoadBalancerReference{ + { + Name: "cluster-con", + Type: providerconfigv1.ClassicLoadBalancerType, + }, + { + Name: "cluster-ext", + Type: providerconfigv1.ClassicLoadBalancerType, + }, + { + Name: "cluster-int", + Type: providerconfigv1.ClassicLoadBalancerType, + }, + }, + } +} + +func stubMachine() (*clusterv1.Machine, *providerconfigv1.AWSMachineProviderConfig, error) { + machinePc := stubProviderConfig() + + codec, err := providerconfigv1.NewCodec() + if err != nil { + return nil, nil, fmt.Errorf("failed creating codec: %v", err) + } + config, err := codec.EncodeProviderConfig(machinePc) + if err != nil { + return nil, nil, fmt.Errorf("encodeToProviderConfig failed: %v", err) + } + + machine := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "aws-actuator-testing-machine", + Namespace: defaultNamespace, + Labels: map[string]string{ + providerconfigv1.ClusterIDLabel: clusterID, + providerconfigv1.MachineRoleLabel: "infra", + providerconfigv1.MachineTypeLabel: "master", + }, + }, + + Spec: clusterv1.MachineSpec{ + ProviderConfig: *config, + }, + } + + return machine, machinePc, nil +} + +func stubMachineAPIResources() (*clusterv1.Machine, *clusterv1.Cluster, *apiv1.Secret, *apiv1.Secret, error) { + awsCredentialsSecret := utils.GenerateAwsCredentialsSecretFromEnv(awsCredentialsSecretName, defaultNamespace) + + userDataSecret := &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: userDataSecretName, + Namespace: defaultNamespace, + }, + Data: map[string][]byte{ + userDataSecretKey: []byte(userDataBlob), + }, + } + + machine, _, err := stubMachine() + if err != nil { + return nil, nil, nil, nil, err + } + + cluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterID, + Namespace: defaultNamespace, + }, + } + + return machine, cluster, awsCredentialsSecret, userDataSecret, nil +} + +func stubInstance(imageID, instanceID string) *ec2.Instance { + return &ec2.Instance{ + ImageId: aws.String(imageID), + InstanceId: aws.String(instanceID), + State: &ec2.InstanceState{ + Name: aws.String("Running"), + Code: aws.Int64(16), + }, + LaunchTime: aws.Time(time.Now()), + PublicDnsName: aws.String("publicDNS"), + PrivateDnsName: aws.String("privateDNS"), + PublicIpAddress: aws.String("1.1.1.1"), + PrivateIpAddress: aws.String("1.1.1.1"), + Tags: []*ec2.Tag{ + { + Key: aws.String("key"), + Value: aws.String("value"), + }, + }, + IamInstanceProfile: &ec2.IamInstanceProfile{ + Id: aws.String("profile"), + }, + SubnetId: aws.String("subnetID"), + Placement: &ec2.Placement{ + AvailabilityZone: aws.String("us-east-1a"), + }, + SecurityGroups: []*ec2.GroupIdentifier{ + { + GroupName: aws.String("groupName"), + }, + }, + } +} + +func stubPCSecurityGroups(groups []providerconfigv1.AWSResourceReference) *providerconfigv1.AWSMachineProviderConfig { + pc := stubProviderConfig() + pc.SecurityGroups = groups + return pc +} + +func stubPCSubnet(subnet providerconfigv1.AWSResourceReference) *providerconfigv1.AWSMachineProviderConfig { + pc := stubProviderConfig() + pc.Subnet = subnet + return pc +} + +func stubPCAMI(ami providerconfigv1.AWSResourceReference) *providerconfigv1.AWSMachineProviderConfig { + pc := stubProviderConfig() + pc.AMI = ami + return pc +}