Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mocking to Cyclops #85

Merged
merged 8 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading