Thou hadst better eat salt with the Philosophers of Greece, than sugar with the Courtiers of Italy.
B. Franklin , Poor Richard's Almanac - 1758 A.D.
The Golang AWS SDK support mocking EC2 instances through the ec2 interface. The following Golang package will feature a function to scan the AWS region for EC2 spot instances and return an array of spot instances:
package aws
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
)
// Client EC2 client interface
type Client struct {
ec2iface.EC2API
}
//New instantiates a Client struct
func New() (*Client, error) {
// Fetch AWS region from EC2 metadata
region, err := ec2metadata.New(session.New()).Region()
if err != nil {
return nil, err
}
// Create an AWS EC2 session using the AWS region
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region)},
)
if err != nil {
return nil, err
}
svc := ec2.New(sess, aws.NewConfig().WithRegion(region))
return &Client{svc}, nil
}
// InstancesList returns array of spot instances in the AWS region
func (c *Client) InstancesList() ([]string, error) {
var spots []string
input := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("instance-lifecycle"),
Values: []*string{
aws.String("spot"),
},
},
},
}
result, err := c.DescribeInstances(input)
for _, reservation := range result.Reservations {
for _, instance := range reservation.Instances {
spots = append(spots, aws.StringValue(instance.PrivateDnsName))
}
}
if err != nil {
return nil, err
}
return spots, nil
}
To test against this function:
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/stretchr/testify/assert"
)
// Define a mock struct to be used in your unit tests of myFunc.
type mockEC2Client struct {
ec2iface.EC2API
resp ec2.DescribeInstancesOutput
result []string
}
func (m *mockEC2Client) DescribeInstances(*ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
return &m.resp, nil
}
func TestInstancesList(t *testing.T) {
cases := []mockEC2Client{
{
resp: ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
{
Instances: []*ec2.Instance{
{
SpotInstanceRequestId: aws.String("speaf-123abc"),
InstanceLifecycle: aws.String("spot"),
PrivateDnsName: aws.String("ip-10-1-102-187.eu-west-1.compute.internal"),
},
{
SpotInstanceRequestId: aws.String("speaf-123abc"),
InstanceLifecycle: aws.String("spot"),
PrivateDnsName: aws.String("ip-10-1-102-188.eu-west-1.compute.internal"),
},
{
SpotInstanceRequestId: aws.String("speaf-123abc"),
InstanceLifecycle: aws.String("spot"),
PrivateDnsName: aws.String("ip-10-1-102-189.eu-west-1.compute.internal"),
},
},
},
},
},
result: []string{"ip-10-1-102-187.eu-west-1.compute.internal", "ip-10-1-102-188.eu-west-1.compute.internal", "ip-10-1-102-189.eu-west-1.compute.internal"},
},
}
for _, c := range cases {
e := Client{
&mockEC2Client{
resp: c.resp,
result: c.result,
},
}
spots, err := e.InstancesList()
if err != nil {
fmt.Println(err)
return
}
assert := assert.New(t)
assert.EqualValues(c.result, spots)
}
}
Note that in this specific case, our test is weak because we’re using Filters
, but you can see how you can mock EC2 instances. Volumes, private instance IPv4 addresses and other EC2 related resources can be added.
Similarly mocking the kubernetes interface can be done through the fake package. In the following example we will create nodes, add labels to the nodes and test against these labels.
package kubernetes
import (
"testing"
"os"
"github.com/private/repo/config"
"github.com/private/repo/log"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
f "k8s.io/client-go/kubernetes/fake"
)
func setupCluster(t *testing.T) Cluster {
t.Helper()
labels := map[string]string{
"k8s.home.co/ScheduleType": "Spot",
"another.k8s.io/Label": "r1",
}
nn1 := "ip-10-1-102-186.eu-west-1.compute.internal"
nn2 := "ip-10-1-102-187.eu-west-1.compute.internal"
nn3 := "ip-10-1-102-188.eu-west-1.compute.internal"
// ObjectMeta definition:
// https://pkg.go.dev/github.com/ericchiang/k8s/apis/meta/v1?tab=doc#ObjectMeta
n1 := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nn1, ClusterName: "testCluster", Labels: labels}}
n2 := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nn2, ClusterName: "testCluster"}}
n3 := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: nn3, ClusterName: "testCluster"}}
c := Cluster{Client: f.NewSimpleClientset()}
_, _ = c.Client.CoreV1().Nodes().Create(n1)
_, _ = c.Client.CoreV1().Nodes().Create(n2)
_, _ = c.Client.CoreV1().Nodes().Create(n3)
return c
}
func TestScan(t *testing.T) {
c := setupCluster(t)
r1 := 2
r2 := 3
l1 := "k8s.home.co/ScheduleType"
l2 := "k8s.home.co/Random"
// Scan() returns the number of nodes that lack label
_, n1, _ := c.Scan(l1)
_, n2, _ := c.Scan(l2)
if len(n1.Items) != r1 {
t.Errorf("Expecting %v nodes, got %v nodes", r1, len(n1.Items))
}
if len(n2.Items) != r2 {
t.Errorf("Expecting %v nodes, got %v nodes", r2, len(n2.Items))
}
}
Similarly, we can mock all kubernetes resources.