Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Validate that EC2 instances only have one security group attached
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Shen <[email protected]>
  • Loading branch information
mjlshen committed Feb 1, 2023
1 parent c027549 commit 32741e5
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 13 deletions.
48 changes: 35 additions & 13 deletions pkg/mirrosa/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ import (
"go.uber.org/zap"
)

const instanceDescription = `A ROSA cluster must have the followings
- 3 masters running
- at least 2 infras running for single-AZ, 3 infras for multi-AZ`
const instanceDescription = `A ROSA cluster must have the following:
- 3 control plane instances running
- at least 2 infra instances running for single-AZ, 3 infra instances for multi-AZ`

var _ Component = &Instances{}

type MirrosaInstancesAPIClient interface {
ec2.DescribeInstancesAPIClient
}

type Instances struct {
log *zap.SugaredLogger
InfraName string
MultiAZ bool

Ec2Client Ec2AwsApi
Ec2Client MirrosaInstancesAPIClient
}

func (c *Client) NewInstances() Instances {
Expand Down Expand Up @@ -61,7 +65,7 @@ func (i Instances) Validate(ctx context.Context) error {
}

// MASTER NODES VALIDATIONS
i.log.Info("validating cluster's master nodes")
i.log.Info("validating cluster's control plane instances")
var masters []types.Instance
masterPattern := fmt.Sprintf("%s-master", i.InfraName)
for _, v := range instances {
Expand All @@ -74,18 +78,24 @@ func (i Instances) Validate(ctx context.Context) error {

// Each cluster has 3 master nodes by default - immutable
if len(masters) != 3 {
return fmt.Errorf("there should be 3 masters belong to the cluster")
return fmt.Errorf("there should be 3 control plane instances, found %d", len(masters))
}

// Check if masters are running
for _, v := range masters {
if v.State.Name != types.InstanceStateNameRunning {
return fmt.Errorf("found non running master instance: %s", *v.InstanceId)
return fmt.Errorf("found non running control plane instance: %s", *v.InstanceId)
}

if len(v.SecurityGroups) != 1 {
return fmt.Errorf("one security group should be attached to %s: (%s-master-sg), got %d", *v.InstanceId, i.InfraName, len(v.SecurityGroups))
}

// TODO: Check if the security group is the correct one, with tag "Name: ${infra_name}-master-sg"
}

// INFRA NODES VALIDATIONS
i.log.Info("validating cluster's infra nodes")
i.log.Info("validating cluster's infra instances")
var infraNodes []types.Instance
infraPattern := fmt.Sprintf("%s-infra", i.InfraName)
for _, v := range instances {
Expand All @@ -97,22 +107,28 @@ func (i Instances) Validate(ctx context.Context) error {
}

if i.MultiAZ && len(infraNodes) < 3 {
return fmt.Errorf("there should be at least 3 infra nodes for multi-AZ clusters")
return fmt.Errorf("there should be at least 3 infra instances for multi-AZ clusters")
}

if !i.MultiAZ && len(infraNodes) < 2 {
return fmt.Errorf("there should be at least 2 infra nodes for single-AZ clusters")
return fmt.Errorf("there should be at least 2 infra instances for single-AZ clusters")
}

// Check if infras are running
for _, v := range infraNodes {
if v.State.Name != types.InstanceStateNameRunning {
return fmt.Errorf("found non running infra node: %s", *v.InstanceId)
return fmt.Errorf("found non running infra instances: %s", *v.InstanceId)
}

if len(v.SecurityGroups) != 1 {
return fmt.Errorf("one security group should be attached to %s: (%s-worker-sg), got %d", *v.InstanceId, i.InfraName, len(v.SecurityGroups))
}

// TODO: Check if the security group is the correct one, with tag "Name: ${infra_name}-worker-sg"
}

// WORKER NODES VALIDATIONS
i.log.Info("validating cluster's worker nodes")
i.log.Info("validating cluster's worker instances")
var workerNodes []types.Instance
workerPattern := fmt.Sprintf("%s-worker", i.InfraName)
for _, v := range instances {
Expand All @@ -133,6 +149,12 @@ func (i Instances) Validate(ctx context.Context) error {
if v.State.Name != types.InstanceStateNameRunning {
i.log.Infof("[error but not blocker]: found non running worker nodes: %s", *v.InstanceId)
}

if len(v.SecurityGroups) != 1 {
return fmt.Errorf("one security group should be attached to %s: (%s-worker-sg), got %d", *v.InstanceId, i.InfraName, len(v.SecurityGroups))
}

// TODO: Check if the security group is the correct one, with tag "Name: ${infra_name}-worker-sg"
}

return nil
Expand All @@ -143,5 +165,5 @@ func (i Instances) Documentation() string {
}

func (i Instances) FilterValue() string {
return "instance validation service"
return "EC2 Instance"
}
147 changes: 147 additions & 0 deletions pkg/mirrosa/instances_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package mirrosa

import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
"go.uber.org/zap/zaptest"
"testing"

"github.com/aws/aws-sdk-go-v2/service/ec2"
)

type mockMirrosaInstancesAPIClient struct {
describeInstancesResp *ec2.DescribeInstancesOutput
}

func (m mockMirrosaInstancesAPIClient) DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(options *ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
return m.describeInstancesResp, nil
}

func TestInstances_Validate(t *testing.T) {
tests := []struct {
name string
instances *Instances
expectErr bool
}{
{
name: "no instances",
instances: &Instances{
log: zaptest.NewLogger(t).Sugar(),
Ec2Client: &mockMirrosaInstancesAPIClient{
describeInstancesResp: &ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{},
},
},
},
expectErr: true,
},
{
name: "healthy multi-az",
instances: &Instances{
log: zaptest.NewLogger(t).Sugar(),
InfraName: "mock",
MultiAZ: true,
Ec2Client: &mockMirrosaInstancesAPIClient{
describeInstancesResp: &ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{
{
Groups: nil,
Instances: []types.Instance{
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-master1")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-master2")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-master3")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-infra1")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-infra2")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-infra3")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-worker1")},
},
},
{
SecurityGroups: []types.GroupIdentifier{
{},
},
State: &types.InstanceState{Name: types.InstanceStateNameRunning},
Tags: []types.Tag{
{Key: aws.String("Name"), Value: aws.String("mock-worker2")},
},
},
},
},
},
},
},
},
expectErr: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.instances.Validate(context.TODO())
if err != nil {
if !test.expectErr {
t.Errorf("expected no err, got %v", err)
}
} else {
if test.expectErr {
t.Error("expected err, got nil")
}
}
})
}
}

0 comments on commit 32741e5

Please sign in to comment.