Skip to content

Commit

Permalink
Create a canned resouces to runtime create k8s cluster.
Browse files Browse the repository at this point in the history
Fixed #56

This is still a wip but I think, this is the right direction

Signed-off-by: Gianluca Arbezzano <gianarb92@gmail.com>
  • Loading branch information
Gianluca Arbezzano committed Jun 5, 2019
1 parent 9b9f289 commit 7c5118d
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 12 deletions.
4 changes: 1 addition & 3 deletions .travis.yml
@@ -1,8 +1,6 @@
language: go
go:
- 1.11.4

install: true
- 1.12.4

services:
- docker
Expand Down
159 changes: 159 additions & 0 deletions canned/kind.go
@@ -0,0 +1,159 @@
package canned

import (
"context"
"fmt"
"io"
"math/rand"
"time"

"github.com/docker/docker/api/types/filters"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
testcontainers "github.com/testcontainers/testcontainers-go"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/kind/pkg/cluster"
"sigs.k8s.io/kind/pkg/cluster/config/encoding"
"sigs.k8s.io/kind/pkg/cluster/create"
"sigs.k8s.io/kind/pkg/util"
)

// seededRand provides the rand function to calculate a different name for
// every kind cluster.
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))

// namePrefix is the fixed part that the container name created by kind will
// have
const namePrefix = "testcontainers-"

// dataPlanFromKind is the suffix kind attaches to all the containers
const dataPlanFromKind = "-control-plane"

// Implement interfaces
var _ testcontainers.Container = (*KubeKindContainer)(nil)

// KubeKindContainer is the struct that spins up a Kubernetes Cluster via
// Kind.
type KubeKindContainer struct {
container testcontainers.Container
kindContext *cluster.Context
clientset *kubernetes.Clientset
clusterName string
}

func (k *KubeKindContainer) Endpoint(ctx context.Context, endpoint string) (string, error) {
return k.container.Endpoint(ctx, endpoint)
}

func (k *KubeKindContainer) PortEndpoint(ctx context.Context, port nat.Port, s string) (string, error) {
return k.container.PortEndpoint(ctx, port, s)
}

func (k *KubeKindContainer) Host(ctx context.Context) (string, error) {
return k.container.Host(ctx)
}

func (k *KubeKindContainer) MappedPort(ctx context.Context, p nat.Port) (nat.Port, error) {
return k.container.MappedPort(ctx, p)
}

func (k *KubeKindContainer) Ports(ctx context.Context) (nat.PortMap, error) {
return k.container.Ports(ctx)
}

func (k *KubeKindContainer) SessionID() string {
return k.container.SessionID()
}

// GetContainerID returns the container ID from Docker
func (k *KubeKindContainer) GetContainerID() string {
return k.container.GetContainerID()
}

// GetClientset returns a configured Kubernetes Clientset ready to be used.
func (k *KubeKindContainer) GetClientset() (*kubernetes.Clientset, error) {
if k.clientset != nil {
return k.clientset, nil
}
config, err := clientcmd.BuildConfigFromFlags("", k.kindContext.KubeConfigPath())
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
k.clientset = clientset
return k.clientset, nil
}

// Start creates and runs the container.
func (k *KubeKindContainer) Start(ctxMain context.Context) error {
renameAttempt := 0
rename:
k.clusterName = fmt.Sprintf("%s%d", namePrefix, seededRand.Int())
kubeconfig := ""
cfg, err := encoding.Load(kubeconfig)
if err != nil {
return errors.Wrap(err, "error loading config")
}

// validate the config.
err = cfg.Validate()
if err != nil {
configErrors := err.(util.Errors)
return configErrors.Errors()[0]
}

known, err := cluster.IsKnown(k.clusterName)
if err != nil {
return err
}
if known {
if renameAttempt > 9 {
return errors.Errorf("a cluster with the name %q already exists", k.clusterName)
}
renameAttempt = renameAttempt + 1
goto rename
}
k.kindContext = cluster.NewContext(k.clusterName)
if err = k.kindContext.Create(cfg,
create.Retain(false),
create.WaitForReady(time.Minute*5),
); err != nil {
return errors.Wrap(err, "failed to create cluster")
}
args := filters.NewArgs(filters.Arg("name", k.clusterName+dataPlanFromKind))
if err != nil {
return err
}
p, err := testcontainers.NewDockerProvider()
if err != nil {
return err
}
c, err := p.ContainerFromDockerArgs(ctxMain, args)
if err != nil {
return err
}
k.container = c
return nil
}

// Logs returns a Reader with the logs from the container.
func (k *KubeKindContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
return k.container.Logs(ctx)
}

// Name returns the name of Kind cluster (not the container).
func (k *KubeKindContainer) Name(ctxMain context.Context) (string, error) {
return k.clusterName, nil
}

// Terminate deletes the cluster via Kind.
func (k *KubeKindContainer) Terminate(ctxMain context.Context) error {
if err := k.kindContext.Delete(); err != nil {
return errors.Wrap(err, "failed to delete cluster")
}
return nil
}
104 changes: 104 additions & 0 deletions canned/kind_test.go
@@ -0,0 +1,104 @@
package canned

import (
"context"
"io/ioutil"
"strings"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestKindCreationAndTermination(t *testing.T) {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
t.Fatal(err.Error())
}
time.Sleep(2 * time.Second)
err = k.Terminate(ctx)
if err != nil {
t.Fatal(err.Error())
}
}

func TestGetDefaultNamespace(t *testing.T) {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
t.Fatal(err.Error())
}
defer k.Terminate(ctx)
clientset, err := k.GetClientset()
if err != nil {
t.Fatal(err.Error())
}
ns, err := clientset.CoreV1().Namespaces().Get("default", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if ns.GetName() != "default" {
t.Fatalf("Expected default namespace got %s", ns.GetName())
}
}

func TestGetLogs(t *testing.T) {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
t.Fatal(err.Error())
}
defer k.Terminate(ctx)
reader, err := k.Logs(ctx)
if err != nil {
t.Fatal(err.Error())
}
b, err := ioutil.ReadAll(reader)
logs := string(b)
if !strings.Contains(logs, "Initializing machine ID from random generator") {
t.Fatal("Didn't find the sentence \"Initializing machine ID from random generator\"")
}
}

func ExampleKubeKindContainer_Start() {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
panic(err.Error())
}
}

func ExampleKubeKindContainer_Terminate() {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
panic(err.Error())
}
defer k.Terminate(ctx)
}

func ExampleKubeKindContainer_Logs() {
ctx := context.Background()
k := &KubeKindContainer{}
err := k.Start(ctx)
if err != nil {
panic(err.Error())
}
defer k.Terminate(ctx)

reader, err := k.Logs(ctx)
if err != nil {
panic(err.Error())
}
b, err := ioutil.ReadAll(reader)
logs := string(b)
if !strings.Contains(logs, "Reached target") {
panic("Reached target")
}
}
18 changes: 18 additions & 0 deletions docker.go
Expand Up @@ -10,6 +10,8 @@ import (
"os/exec"
"strings"

"github.com/docker/docker/api/types/filters"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
Expand Down Expand Up @@ -203,6 +205,22 @@ func NewDockerProvider() (*DockerProvider, error) {
return p, nil
}

func (p *DockerProvider) ContainerFromDockerArgs(ctx context.Context, args filters.Args) (*DockerContainer, error) {
containers, err := p.client.ContainerList(ctx, types.ContainerListOptions{
Filters: args,
})
if err != nil {
return nil, err
}
if len(containers) != 1 {
return nil, errors.Errorf("Impossible to get container associated to this cluster. Cluster terminated.")
}
return &DockerContainer{
ID: containers[0].ID,
provider: p,
}, nil
}

// CreateContainer fulfills a request for a container without starting it
func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) {
exposedPortSet, exposedPortMap, err := nat.ParsePortSpecs(req.ExposedPorts)
Expand Down
11 changes: 8 additions & 3 deletions go.mod
Expand Up @@ -9,19 +9,24 @@ require (
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.3.3 // indirect
github.com/go-sql-driver/mysql v1.4.1
github.com/gogo/protobuf v1.2.0 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/kardianos/govendor v1.0.9 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/moby/moby v1.13.1
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pkg/errors v0.8.0
github.com/prometheus/common v0.4.1
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.2.0 // indirect
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/grpc v1.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gotest.tools v2.2.0+incompatible // indirect
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
k8s.io/client-go v11.0.0+incompatible
k8s.io/utils v0.0.0-20190520173318-324c5df7d3f0 // indirect
sigs.k8s.io/kind v0.3.0
)

0 comments on commit 7c5118d

Please sign in to comment.