Convalesco

Current revision: 0.7

Last update: 2020-06-24 20:47:52 +0000 UTC

Civilization is the limitless multiplication of unecessary necessities.

M. Twain


Mocking EC2 and kubernetes API interfaces in Golang

Date: 06/05/2020, 21:18

Category: technology

Revision: 1



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.