Skip to content

Commit

Permalink
Merge pull request #85 from atlassian-labs/vportella/add-mocking-to-c…
Browse files Browse the repository at this point in the history
…yclops

Add mocking to Cyclops
  • Loading branch information
vincentportella authored Aug 12, 2024
2 parents 78c3d43 + f3f0ef4 commit 81491a7
Show file tree
Hide file tree
Showing 10 changed files with 836 additions and 59 deletions.
4 changes: 2 additions & 2 deletions pkg/cloudprovider/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ func verifyIfErrorOccurredWithDefaults(apiErr error, expectedMessage string) (bo
}

type provider struct {
autoScalingService *autoscaling.AutoScaling
ec2Service *ec2.EC2
autoScalingService autoscalingiface.AutoScalingAPI
ec2Service ec2iface.EC2API
logger logr.Logger
}

Expand Down
9 changes: 9 additions & 0 deletions pkg/cloudprovider/aws/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/atlassian-labs/cyclops/pkg/cloudprovider"
fakeaws "github.com/atlassian-labs/cyclops/pkg/cloudprovider/aws/fake"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -40,3 +41,11 @@ func NewCloudProvider(logger logr.Logger) (cloudprovider.CloudProvider, error) {

return p, nil
}

// NewGenericCloudProvider returns a new mock AWS cloud provider
func NewGenericCloudProvider(autoscalingiface *fakeaws.Autoscaling, ec2iface *fakeaws.Ec2) cloudprovider.CloudProvider {
return &provider{
autoScalingService: autoscalingiface,
ec2Service: ec2iface,
}
}
142 changes: 142 additions & 0 deletions pkg/cloudprovider/aws/fake/fake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package fakeaws

import (
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/aws/aws-sdk-go/service/ec2"

"github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
)

var (
defaultAvailabilityZone = "us-east-1a"
)

type Instance struct {
InstanceID string
AutoscalingGroupName string
State string
}

type Ec2 struct {
ec2iface.EC2API

Instances map[string]*Instance
}

type Autoscaling struct {
autoscalingiface.AutoScalingAPI

Instances map[string]*Instance
}

func GenerateProviderID(instanceID string) string {
return fmt.Sprintf("aws:///%s/%s",
defaultAvailabilityZone,
instanceID,
)
}

func generateEc2Instance(instance *Instance) *ec2.Instance {
ec2Instance := &ec2.Instance{
InstanceId: aws.String(instance.InstanceID),
State: &ec2.InstanceState{
Name: aws.String(instance.State),
},
}

return ec2Instance
}

func generateAutoscalingInstance(instance *Instance) *autoscaling.Instance {
autoscalingInstance := &autoscaling.Instance{
InstanceId: aws.String(instance.InstanceID),
AvailabilityZone: aws.String(defaultAvailabilityZone),
}

return autoscalingInstance
}

// *************** Autoscaling *************** //

func (m *Autoscaling) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error) {
var asgs = make(map[string]*autoscaling.Group, 0)

var asgNameLookup = make(map[string]interface{})

for _, asgName := range input.AutoScalingGroupNames {
asgNameLookup[*asgName] = nil
}

for _, instance := range m.Instances {
if instance.AutoscalingGroupName == "" {
continue
}

if _, exists := asgNameLookup[instance.AutoscalingGroupName]; !exists {
continue
}

asg, exists := asgs[instance.AutoscalingGroupName]

if !exists {
asg = &autoscaling.Group{
AutoScalingGroupName: aws.String(instance.AutoscalingGroupName),
Instances: []*autoscaling.Instance{},
AvailabilityZones: []*string{
aws.String(defaultAvailabilityZone),
},
}

asgs[instance.AutoscalingGroupName] = asg
}

asg.Instances = append(
asg.Instances,
generateAutoscalingInstance(instance),
)
}

var asgList = make([]*autoscaling.Group, 0)

for _, asg := range asgs {
asgList = append(asgList, asg)
}

return &autoscaling.DescribeAutoScalingGroupsOutput{
AutoScalingGroups: asgList,
}, nil
}

// *************** EC2 *************** //

func (m *Ec2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
var instances = make([]*ec2.Instance, 0)
var instanceIds = make(map[string]interface{})

for _, instanceId := range input.InstanceIds {
instanceIds[*instanceId] = nil
}

for _, instance := range m.Instances {
if _, ok := instanceIds[instance.InstanceID]; input.InstanceIds != nil && !ok {
continue
}

instances = append(
instances,
generateEc2Instance(instance),
)
}

return &ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
Instances: instances,
},
},
}, nil
}
61 changes: 61 additions & 0 deletions pkg/controller/cyclenoderequest/transitioner/test_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package transitioner

import (
"net/http"

v1 "github.com/atlassian-labs/cyclops/pkg/apis/atlassian/v1"
"github.com/atlassian-labs/cyclops/pkg/controller"
"github.com/atlassian-labs/cyclops/pkg/mock"
)

type Option func(t *Transitioner)

func WithCloudProviderInstances(nodes []*mock.Node) Option {
return func(t *Transitioner) {
t.cloudProviderInstances = append(t.cloudProviderInstances, nodes...)
}
}

func WithKubeNodes(nodes []*mock.Node) Option {
return func(t *Transitioner) {
t.kubeNodes = append(t.kubeNodes, nodes...)
}
}

// ************************************************************************** //

type Transitioner struct {
*CycleNodeRequestTransitioner
*mock.Client

cloudProviderInstances []*mock.Node
kubeNodes []*mock.Node
}

func NewFakeTransitioner(cnr *v1.CycleNodeRequest, opts ...Option) *Transitioner {
t := &Transitioner{
// By default there are no nodes and each test will
// override these as needed
cloudProviderInstances: make([]*mock.Node, 0),
kubeNodes: make([]*mock.Node, 0),
}

for _, opt := range opts {
opt(t)
}

t.Client = mock.NewClient(t.kubeNodes, t.cloudProviderInstances, cnr)

rm := &controller.ResourceManager{
Client: t.K8sClient,
RawClient: t.RawClient,
HttpClient: http.DefaultClient,
CloudProvider: t.CloudProvider,
}

t.CycleNodeRequestTransitioner = NewCycleNodeRequestTransitioner(
cnr, rm, Options{},
)

return t
}
12 changes: 10 additions & 2 deletions pkg/controller/cyclenoderequest/transitioner/transitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,23 @@ func (t *CycleNodeRequestTransitioner) transitionPending() (reconcile.Result, er
return t.transitionToHealing(fmt.Errorf("no existing nodes in cloud provider matched selector"))
}

nodeGroupNames := t.cycleNodeRequest.GetNodeGroupNames()

// Describe the node group for the request
t.rm.LogEvent(t.cycleNodeRequest, "FetchingNodeGroup", "Fetching node group: %v", t.cycleNodeRequest.GetNodeGroupNames())
nodeGroups, err := t.rm.CloudProvider.GetNodeGroups(t.cycleNodeRequest.GetNodeGroupNames())
t.rm.LogEvent(t.cycleNodeRequest, "FetchingNodeGroup", "Fetching node group: %v", nodeGroupNames)

if len(nodeGroupNames) == 0 {
return t.transitionToHealing(fmt.Errorf("must have at least one nodegroup name defined"))
}

nodeGroups, err := t.rm.CloudProvider.GetNodeGroups(nodeGroupNames)
if err != nil {
return t.transitionToHealing(err)
}

// get instances inside cloud provider node groups
nodeGroupInstances := nodeGroups.Instances()

// Do some sanity checking before we start filtering things
// Check the instance count of the node group matches the number of nodes found in Kubernetes
if len(kubeNodes) != len(nodeGroupInstances) {
Expand Down
Loading

0 comments on commit 81491a7

Please sign in to comment.