Skip to content

Commit

Permalink
fix(k8s,mode/helm): add unit tests for helm mode (#182)
Browse files Browse the repository at this point in the history
Fixes #158
  • Loading branch information
arschles committed Sep 14, 2016
1 parent e193008 commit a46a55d
Show file tree
Hide file tree
Showing 17 changed files with 443 additions and 18 deletions.
52 changes: 52 additions & 0 deletions k8s/fake_config_maps_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package k8s

import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/watch"
)

// FakeConfigMapsInterface is a fake version of (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface, for use in unit tests
type FakeConfigMapsInterface struct {
Created []*api.ConfigMap
GetReturns map[string]*api.ConfigMap
}

// NewFakeConfigMapsInterface returns a new, empty *FakeConfigMapsInterface
func NewFakeConfigMapsInterface() *FakeConfigMapsInterface {
return &FakeConfigMapsInterface{Created: nil, GetReturns: make(map[string]*api.ConfigMap)}
}

// Get is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. If name is in f.GetReturns, returns f.GetReturns[name], nil. Otherwise returns nil, nil
func (f *FakeConfigMapsInterface) Get(name string) (*api.ConfigMap, error) {
cm, ok := f.GetReturns[name]
if ok {
return cm, nil
}
return nil, nil
}

// List is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. It currently is not implemented and returns nil, nil. It will be implemented if the need arises in tests
func (f *FakeConfigMapsInterface) List(opts api.ListOptions) (*api.ConfigMapList, error) {
return nil, nil
}

// Create is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. It appends cm to f.Created and then returns cm, nil. This function is not concurrency-safe
func (f *FakeConfigMapsInterface) Create(cm *api.ConfigMap) (*api.ConfigMap, error) {
f.Created = append(f.Created, cm)
return cm, nil
}

// Delete is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. It currently is not implemented and returns nil. It will be implemented if the need arises in tests
func (f *FakeConfigMapsInterface) Delete(string) error {
return nil
}

// Update is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. It currently is not implemented and returns nil, nil. It will be implemented if the need arises in tests
func (f *FakeConfigMapsInterface) Update(*api.ConfigMap) (*api.ConfigMap, error) {
return nil, nil
}

// Watch is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsInterface interface implementation. It currently is not implemented and returns nil, nil. It will be implemented if the need arises in tests
func (f *FakeConfigMapsInterface) Watch(api.ListOptions) (watch.Interface, error) {
return nil, nil
}
29 changes: 29 additions & 0 deletions k8s/fake_config_maps_namespacer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package k8s

import (
kcl "k8s.io/kubernetes/pkg/client/unversioned"
)

// FakeConfigMapsNamespacer is a fake implementation of (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsNamespacer, suitable for use in unit tests.
type FakeConfigMapsNamespacer struct {
ToReturn map[string]*FakeConfigMapsInterface
Returned map[string]*FakeConfigMapsInterface
}

// NewFakeConfigMapsNamespacer returns an empty FakeConfigMapsNamespacer
func NewFakeConfigMapsNamespacer() *FakeConfigMapsNamespacer {
return &FakeConfigMapsNamespacer{
ToReturn: make(map[string]*FakeConfigMapsInterface),
Returned: make(map[string]*FakeConfigMapsInterface),
}
}

// ConfigMaps is the (k8s.io/kubernetes/pkg/client/unversioned).ConfigMapsNamespacer interface implementation. It returns an empty kcl.ConfigMapsInterface
func (f *FakeConfigMapsNamespacer) ConfigMaps(ns string) kcl.ConfigMapsInterface {
iface, ok := f.ToReturn[ns]
if !ok {
iface = &FakeConfigMapsInterface{}
}
f.Returned[ns] = iface
return iface
}
55 changes: 55 additions & 0 deletions mode/helm/binder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package helm

import (
"fmt"
"testing"

"github.com/arschles/assert"
"github.com/deis/steward/k8s"
"github.com/pborman/uuid"
"k8s.io/helm/pkg/chartutil"
"k8s.io/kubernetes/pkg/api"
)

func TestDataFieldKey(t *testing.T) {
const (
key = "key1"
)
cm := &api.ConfigMap{
ObjectMeta: api.ObjectMeta{Name: uuid.New(), Namespace: uuid.New()},
}
fullKey := dataFieldKey(cm, key)
assert.Equal(t, fullKey, fmt.Sprintf("%s-%s-%s", cm.Namespace, cm.Name, key), "data field key")
}

func TestBind(t *testing.T) {
const (
instID = "testInstID"
bindID = "testBindID"
cmNamespace = "default" // this is the creds config map namespace hard-coded in the alpine chart
cmName = "my-creds" // this is the creds config map name hard-coded in the alpine chart
)
chart, err := chartutil.Load(alpineChartLoc())
assert.NoErr(t, err)
nsr := k8s.NewFakeConfigMapsNamespacer()
defaultIface := k8s.NewFakeConfigMapsInterface()
defaultIface.GetReturns[cmName] = &api.ConfigMap{
ObjectMeta: api.ObjectMeta{
Name: "testName",
Namespace: "testNamespace",
},
Data: map[string]string{"testKey": "testVal"},
}
nsr.ToReturn[cmNamespace] = defaultIface
binder, err := newBinder(chart, nsr)
assert.NoErr(t, err)
resp, err := binder.Bind(instID, bindID, nil)
assert.NoErr(t, err)
assert.NotNil(t, resp, "bind response")
assert.Equal(t, len(resp.Creds), len(defaultIface.GetReturns[cmName].Data), "length of returned credentials")
expectedCM := defaultIface.GetReturns[cmName]
for k, v := range expectedCM.Data {
expectedKey := dataFieldKey(expectedCM, k)
assert.Equal(t, v, resp.Creds[expectedKey], fmt.Sprintf("value of key %s", k))
}
}
31 changes: 31 additions & 0 deletions mode/helm/cataloger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package helm

import (
"testing"

"github.com/arschles/assert"
)

func TestCatalogerList(t *testing.T) {
cfg := &config{
ServiceID: "svcID1",
ServiceName: "svc1",
ServiceDescription: "this is service 1",
PlanID: "planID1",
PlanName: "plan1",
PlanDescription: "this is plan 1",
}
cat := newCataloger(cfg)
svcs, err := cat.List()
assert.NoErr(t, err)
assert.Equal(t, len(svcs), 1, "number of listed services")
svc := svcs[0]
assert.Equal(t, svc.ID, cfg.ServiceID, "service ID")
assert.Equal(t, svc.Name, cfg.ServiceName, "service name")
assert.Equal(t, svc.Description, cfg.ServiceDescription, "service description")
assert.Equal(t, len(svc.Plans), 1, "number of plans")
plan := svc.Plans[0]
assert.Equal(t, plan.ID, cfg.PlanID, "plan ID")
assert.Equal(t, plan.Name, cfg.PlanName, "plan name")
assert.Equal(t, plan.Description, cfg.PlanDescription, "plan description")
}
14 changes: 14 additions & 0 deletions mode/helm/chart_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package helm

import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
Expand All @@ -13,6 +14,15 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart"
)

type errChartNotFound struct {
chartURL string
code int
}

func (e errChartNotFound) Error() string {
return fmt.Sprintf("chart at %s not found (status code %d)", e.chartURL, e.code)
}

// getChart downloads the chart at chartURL to a directory, parses it into a *chart.Chart and returns it along with the root directory of the directory the chart was downloaded to. Returns a non-nil error if the parsing failed. It's the caller's responsibility to delete the chart directory when done with it.
func getChart(ctx context.Context, httpCl *http.Client, chartURL string) (*chart.Chart, string, error) {
logger.Debugf("downloading chart from %s", chartURL)
Expand All @@ -22,6 +32,10 @@ func getChart(ctx context.Context, httpCl *http.Client, chartURL string) (*chart
return nil, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Errorf("got status code %d trying to download chart at %s", resp.StatusCode, chartURL)
return nil, "", errChartNotFound{chartURL: chartURL, code: resp.StatusCode}
}
tmpDir, err := ioutil.TempDir("", chartTmpDirPrefix)
if err != nil {
return nil, "", err
Expand Down
74 changes: 74 additions & 0 deletions mode/helm/chart_util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package helm

import (
"bytes"
"context"
"io"
"io/ioutil"
"net/http"
"os"
"testing"
"time"

"github.com/arschles/assert"
"github.com/arschles/testsrv"
)

var (
bgCtx = context.Background()
)

func stdHandler(b []byte) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, bytes.NewBuffer(b))
})
}

func notFoundHandler([]byte) http.Handler {
return http.NotFoundHandler()
}

func newChartServer(createHdl func([]byte) http.Handler) (*testsrv.Server, error) {
fb, err := ioutil.ReadFile(alpineChartLoc())
if err != nil {
return nil, err
}

hdl := createHdl(fb)
return testsrv.StartServer(hdl), nil
}

func TestGetChartCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(bgCtx)
cancel()
srv, err := newChartServer(stdHandler)
assert.NoErr(t, err)
defer srv.Close()
_, tmpDir, err := getChart(ctx, http.DefaultClient, srv.URLStr())
defer os.RemoveAll(tmpDir)
assert.True(t, err != nil, "gettting chart with cancelled context returned no error")
srv.AcceptN(1, 1*time.Second)
}

func TestGetChartNotFound(t *testing.T) {
srv, err := newChartServer(notFoundHandler)
assert.NoErr(t, err)
defer srv.Close()
_, _, err = getChart(bgCtx, http.DefaultClient, srv.URLStr())
if _, ok := err.(errChartNotFound); !ok {
t.Fatalf("returned error was not a errChartNotFound")
}
}

func TestGetChart(t *testing.T) {
srv, err := newChartServer(stdHandler)
assert.NoErr(t, err)
defer srv.Close()
go func() {
srv.AcceptN(1, 1*time.Second)
}()
ch, tmpDir, err := getChart(bgCtx, http.DefaultClient, srv.URLStr())
defer os.RemoveAll(tmpDir)
assert.NoErr(t, err)
assert.NotNil(t, ch, "chart")
}
15 changes: 15 additions & 0 deletions mode/helm/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package helm

import (
"path/filepath"

"github.com/juju/loggo"
)

func init() {
logger.SetLogLevel(loggo.TRACE)
}

func alpineChartLoc() string {
return filepath.Join("..", "..", "testdata", "alpine-0.1.1.tgz")
}
38 changes: 38 additions & 0 deletions mode/helm/deprovisioner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package helm

import (
"testing"

"github.com/arschles/assert"
"github.com/deis/steward/mode"
"k8s.io/helm/pkg/proto/hapi/chart"
)

func TestDeprovisionNoop(t *testing.T) {
deprov := newDeprovisioner(nil, ProvisionBehaviorNoop, nil)
resp, err := deprov.Deprovision("testInstanceID", nil)
assert.NoErr(t, err)
assert.Equal(t, resp.Operation, deprovisionedNoopOperation, "operation")
}
func TestDeprovisionActive(t *testing.T) {
const (
targetNS = "testNamespace"
instID = "testInstanceID"
releaseName = "testRelease"
)
chart := &chart.Chart{}
deleter := &fakeCreatorDeleter{}
deprov := newDeprovisioner(chart, ProvisionBehaviorActive, deleter)
deprovReq := &mode.DeprovisionRequest{
Parameters: mode.JSONObject(map[string]string{releaseNameKey: releaseName}),
}
resp, err := deprov.Deprovision(instID, deprovReq)
assert.NoErr(t, err)
assert.NotNil(t, resp, "deprovision response")
assert.Equal(t, len(deleter.createCalls), 0, "number of calls to create")
assert.Equal(t, len(deleter.deleteCalls), 1, "number of calls to delete")
deleteCall := deleter.deleteCalls[0]
assert.Equal(t, deleteCall, releaseName, "target namespace of create call")
assert.Equal(t, resp.Operation, deprovisionedActiveOperation, "release name")

}
30 changes: 30 additions & 0 deletions mode/helm/fake_creator_deleter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package helm

import (
"k8s.io/helm/pkg/proto/hapi/chart"
rls "k8s.io/helm/pkg/proto/hapi/services"
)

type createCall struct {
chart *chart.Chart
installNS string
}

type fakeCreatorDeleter struct {
createResp *rls.InstallReleaseResponse
createRespErr error
deleteResp *rls.UninstallReleaseResponse
deleteRespErr error
createCalls []*createCall
deleteCalls []string
}

func (f *fakeCreatorDeleter) Create(chart *chart.Chart, installNS string) (*rls.InstallReleaseResponse, error) {
f.createCalls = append(f.createCalls, &createCall{chart: chart, installNS: installNS})
return f.createResp, f.createRespErr
}

func (f *fakeCreatorDeleter) Delete(releaseName string) (*rls.UninstallReleaseResponse, error) {
f.deleteCalls = append(f.deleteCalls, releaseName)
return f.deleteResp, f.deleteRespErr
}
1 change: 1 addition & 0 deletions mode/helm/helm_config_maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func rangeConfigMaps(
cmInfos []cmNamespaceAndName,
fn func(*api.ConfigMap) error) error {
for _, cmInfo := range cmInfos {
logger.Debugf("getting config map %s/%s", cmInfo.Namespace, cmInfo.Name)
cm, err := cmNamespacer.ConfigMaps(cmInfo.Namespace).Get(cmInfo.Name)
if err != nil {
logger.Debugf("no such ConfigMap %s/%s", cmInfo.Namespace, cmInfo.Name)
Expand Down
23 changes: 23 additions & 0 deletions mode/helm/helm_config_maps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package helm

import (
"testing"

"github.com/arschles/assert"
"github.com/deis/steward/k8s"
"k8s.io/kubernetes/pkg/api"
)

func TestRangeConfigMaps(t *testing.T) {
infos := []cmNamespaceAndName{
{Name: "name1", Namespace: "ns1"},
{Name: "name2", Namespace: "ns2"},
}
namespacer := k8s.NewFakeConfigMapsNamespacer()
gathered := []*api.ConfigMap{}
rangeConfigMaps(namespacer, infos, func(cm *api.ConfigMap) error {
gathered = append(gathered, cm)
return nil
})
assert.Equal(t, len(gathered), len(infos), "number of gathered config maps")
}

0 comments on commit a46a55d

Please sign in to comment.