github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/boskos/mason/mason_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mason
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"context"
    28  
    29  	"k8s.io/test-infra/boskos/common"
    30  	"k8s.io/test-infra/boskos/ranch"
    31  	"k8s.io/test-infra/boskos/storage"
    32  )
    33  
    34  var (
    35  	errConstruct = fmt.Errorf("failed to construct")
    36  )
    37  
    38  const (
    39  	fakeConfigType    = "fakeConfig"
    40  	emptyContent      = "empty content"
    41  	owner             = "mason"
    42  	defaultWaitPeriod = 100 * time.Millisecond
    43  )
    44  
    45  func errorsEqual(a, b error) bool {
    46  	if a == nil && b == nil {
    47  		return true
    48  	}
    49  	if a != nil && b != nil {
    50  		return a.Error() == b.Error()
    51  	}
    52  	return false
    53  }
    54  
    55  type releasedResource struct {
    56  	name, state string
    57  }
    58  
    59  type fakeBoskos struct {
    60  	ranch             *ranch.Ranch
    61  	releasedResources chan releasedResource
    62  }
    63  
    64  type testConfig map[string]struct {
    65  	resourceNeeds *common.ResourceNeeds
    66  	count         int
    67  }
    68  
    69  type fakeConfig struct {
    70  	sleepTime time.Duration
    71  	err       error
    72  }
    73  
    74  func fakeConfigConverter(in string) (Masonable, error) {
    75  	return &fakeConfig{sleepTime: 0}, nil
    76  }
    77  
    78  func failingConfigConverter(in string) (Masonable, error) {
    79  	return &fakeConfig{sleepTime: 0, err: errConstruct}, nil
    80  }
    81  
    82  func timeoutConfigConverter(in string) (Masonable, error) {
    83  	return &fakeConfig{sleepTime: defaultWaitPeriod}, nil
    84  }
    85  
    86  func (fc *fakeConfig) Construct(ctx context.Context, res common.Resource, typeToRes common.TypeToResources) (*common.UserData, error) {
    87  	// Mess around with data
    88  	res.Name = "nothingToDo"
    89  	res.State = "unknown"
    90  	res.UserData = common.UserDataFromMap(common.UserDataMap{"test": "test"})
    91  	for k := range typeToRes {
    92  		delete(typeToRes, k)
    93  	}
    94  	time.Sleep(fc.sleepTime)
    95  	return common.UserDataFromMap(common.UserDataMap{"fakeConfig": "unused"}), fc.err
    96  }
    97  
    98  // Create a fake client
    99  func createFakeBoskos(tc testConfig) (*ranch.Storage, *Client, []common.ResourcesConfig, chan releasedResource) {
   100  	names := make(chan releasedResource, 100)
   101  	configNames := map[string]bool{}
   102  	var configs []common.ResourcesConfig
   103  	s, _ := ranch.NewStorage(storage.NewMemoryStorage(), "")
   104  	r, _ := ranch.NewRanch("", s)
   105  
   106  	for rtype, c := range tc {
   107  		for i := 0; i < c.count; i++ {
   108  			res := common.Resource{
   109  				Type:     rtype,
   110  				Name:     fmt.Sprintf("%s_%d", rtype, i),
   111  				State:    common.Free,
   112  				UserData: &common.UserData{},
   113  			}
   114  			if c.resourceNeeds != nil {
   115  				res.State = common.Dirty
   116  				if _, ok := configNames[rtype]; !ok {
   117  					configNames[rtype] = true
   118  					configs = append(configs, common.ResourcesConfig{
   119  						Config: common.ConfigType{
   120  							Type:    fakeConfigType,
   121  							Content: emptyContent,
   122  						},
   123  						Name:  rtype,
   124  						Needs: *c.resourceNeeds,
   125  					})
   126  				}
   127  			}
   128  			s.AddResource(res)
   129  		}
   130  	}
   131  	return s, NewClient(&fakeBoskos{ranch: r, releasedResources: names}), configs, names
   132  }
   133  
   134  func (fb *fakeBoskos) Acquire(rtype, state, dest string) (*common.Resource, error) {
   135  	return fb.ranch.Acquire(rtype, state, dest, owner)
   136  }
   137  
   138  func (fb *fakeBoskos) AcquireByState(state, dest string, names []string) ([]common.Resource, error) {
   139  	return fb.ranch.AcquireByState(state, dest, owner, names)
   140  }
   141  
   142  func (fb *fakeBoskos) ReleaseOne(name, dest string) error {
   143  	fb.releasedResources <- releasedResource{name: name, state: dest}
   144  	return fb.ranch.Release(name, dest, owner)
   145  }
   146  
   147  func (fb *fakeBoskos) UpdateOne(name, state string, userData *common.UserData) error {
   148  	return fb.ranch.Update(name, owner, state, userData)
   149  }
   150  
   151  func (fb *fakeBoskos) UpdateAll(state string) error {
   152  	// not used in this test
   153  	return nil
   154  }
   155  
   156  func (fb *fakeBoskos) ReleaseAll(state string) error {
   157  	// not used in this test
   158  	return nil
   159  }
   160  
   161  func (fb *fakeBoskos) SyncAll() error {
   162  	// not used in this test
   163  	return nil
   164  }
   165  
   166  func TestRecycleLeasedResources(t *testing.T) {
   167  	tc := testConfig{
   168  		"type1": {
   169  			count: 1,
   170  		},
   171  		"type2": {
   172  			resourceNeeds: &common.ResourceNeeds{
   173  				"type1": 1,
   174  			},
   175  			count: 1,
   176  		},
   177  	}
   178  
   179  	rStorage, mClient, configs, _ := createFakeBoskos(tc)
   180  	res1, _ := rStorage.GetResource("type1_0")
   181  	res1.State = "type2_0"
   182  	rStorage.UpdateResource(res1)
   183  	res2, _ := rStorage.GetResource("type2_0")
   184  	res2.UserData.Set(LeasedResources, &[]string{"type1_0"})
   185  	rStorage.UpdateResource(res2)
   186  	m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   187  	m.storage.SyncConfigs(configs)
   188  	m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter)
   189  	ctx, cancel := context.WithCancel(context.Background())
   190  	m.cancel = cancel
   191  	m.start(ctx, m.recycleAll)
   192  	select {
   193  	case <-m.pending:
   194  		break
   195  	case <-time.After(1 * time.Second):
   196  		t.Errorf("Timeout")
   197  	}
   198  	m.Stop()
   199  	res1, _ = rStorage.GetResource("type1_0")
   200  	res2, _ = rStorage.GetResource("type2_0")
   201  	if res2.State != common.Cleaning {
   202  		t.Errorf("Resource state should be cleaning, found %s", res2.State)
   203  	}
   204  	if res1.State != common.Dirty {
   205  		t.Errorf("Resource state should be dirty, found %s", res1.State)
   206  	}
   207  }
   208  
   209  func TestRecycleNoLeasedResources(t *testing.T) {
   210  	tc := testConfig{
   211  		"type1": {
   212  			count: 1,
   213  		},
   214  		"type2": {
   215  			resourceNeeds: &common.ResourceNeeds{
   216  				"type1": 1,
   217  			},
   218  			count: 1,
   219  		},
   220  	}
   221  
   222  	rStorage, mClient, configs, _ := createFakeBoskos(tc)
   223  	m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   224  	m.storage.SyncConfigs(configs)
   225  	m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter)
   226  	ctx, cancel := context.WithCancel(context.Background())
   227  	m.cancel = cancel
   228  	m.start(ctx, m.recycleAll)
   229  	select {
   230  	case <-m.pending:
   231  		break
   232  	case <-time.After(1 * time.Second):
   233  		t.Errorf("Timeout")
   234  	}
   235  	m.Stop()
   236  	res1, _ := rStorage.GetResource("type1_0")
   237  	res2, _ := rStorage.GetResource("type2_0")
   238  	if res2.State != common.Cleaning {
   239  		t.Errorf("Resource state should be cleaning")
   240  	}
   241  	if res1.State != common.Free {
   242  		t.Errorf("Resource state should be untouched, current %s", mClient.resources["type1_0"].State)
   243  	}
   244  }
   245  
   246  func TestCleanOne(t *testing.T) {
   247  	testCases := []struct {
   248  		name          string
   249  		configConvert ConfigConverter
   250  		err           error
   251  		timeout       bool
   252  	}{
   253  		{
   254  			name:          "success",
   255  			configConvert: fakeConfigConverter,
   256  		},
   257  		{
   258  			name:          "constructFailure",
   259  			configConvert: failingConfigConverter,
   260  			err:           errConstruct,
   261  		},
   262  		{
   263  			name:          "constructTimeout",
   264  			configConvert: timeoutConfigConverter,
   265  			err:           fmt.Errorf("context deadline exceeded"),
   266  			timeout:       true,
   267  		},
   268  	}
   269  
   270  	config := testConfig{
   271  		"type1": {
   272  			count: 1,
   273  		},
   274  		"type2": {
   275  			resourceNeeds: &common.ResourceNeeds{
   276  				"type1": 1,
   277  			},
   278  			count: 1,
   279  		},
   280  	}
   281  
   282  	for _, tc := range testCases {
   283  		t.Run(tc.name, func(tt *testing.T) {
   284  			rStorage, mClient, configs, _ := createFakeBoskos(config)
   285  			m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   286  			m.storage.SyncConfigs(configs)
   287  			m.RegisterConfigConverter(fakeConfigType, tc.configConvert)
   288  			masonRes, err := m.client.Acquire("type2", common.Dirty, common.Cleaning)
   289  			if err != nil {
   290  				t.Errorf("unexpected error %v", err)
   291  				t.FailNow()
   292  			}
   293  			req := requirements{
   294  				resource:    *masonRes,
   295  				needs:       *config["type2"].resourceNeeds,
   296  				fulfillment: common.TypeToResources{},
   297  			}
   298  			ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   299  			defer cancel()
   300  			if err := m.fulfillOne(ctx, &req); err != nil {
   301  				t.Errorf("unexpected error %v", err)
   302  			}
   303  
   304  			if tc.timeout {
   305  				ctx, cancel = context.WithTimeout(context.Background(), defaultWaitPeriod/2)
   306  				defer cancel()
   307  			}
   308  
   309  			err = m.cleanOne(ctx, &req.resource, req.fulfillment)
   310  			if !errorsEqual(tc.err, err) {
   311  				tt.Errorf("expected error %v got %v", tc.err, err)
   312  			}
   313  			m.garbageCollect(req)
   314  			resources, err := rStorage.GetResources()
   315  			if err != nil {
   316  				t.Errorf("unexpected error %v", err)
   317  				t.FailNow()
   318  			}
   319  			for _, res := range resources {
   320  				if res.State != common.Dirty {
   321  					tt.Errorf("resource %v should be released as dirty", res)
   322  				}
   323  
   324  			}
   325  		})
   326  	}
   327  }
   328  
   329  func TestFulfillOne(t *testing.T) {
   330  	tc := testConfig{
   331  		"type1": {
   332  			count: 1,
   333  		},
   334  		"type2": {
   335  			resourceNeeds: &common.ResourceNeeds{
   336  				"type1": 1,
   337  			},
   338  			count: 1,
   339  		},
   340  	}
   341  
   342  	rStorage, mClient, configs, _ := createFakeBoskos(tc)
   343  	m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   344  	m.storage.SyncConfigs(configs)
   345  	res, _ := mClient.basic.Acquire("type2", common.Dirty, common.Cleaning)
   346  	conf, err := m.storage.GetConfig("type2")
   347  	if err != nil {
   348  		t.Error("failed to get config")
   349  	}
   350  	req := requirements{
   351  		resource:    *res,
   352  		needs:       conf.Needs,
   353  		fulfillment: common.TypeToResources{},
   354  	}
   355  	if err = m.fulfillOne(context.Background(), &req); err != nil {
   356  		t.Errorf("could not satisfy requirements ")
   357  	}
   358  	if len(req.fulfillment) != 1 {
   359  		t.Errorf("there should be only one type")
   360  	}
   361  	if len(req.fulfillment["type1"]) != 1 {
   362  		t.Errorf("there should be only one resources")
   363  	}
   364  	userRes := req.fulfillment["type1"][0]
   365  	leasedResource, _ := rStorage.GetResource(userRes.Name)
   366  	if leasedResource.State != common.Leased {
   367  		t.Errorf("State should be Leased")
   368  	}
   369  	*res, _ = rStorage.GetResource(res.Name)
   370  	var leasedResources common.LeasedResources
   371  	if res.UserData.Extract(LeasedResources, &leasedResources); err != nil {
   372  		t.Errorf("unable to extract %s", LeasedResources)
   373  	}
   374  	if res.UserData.ToMap()[LeasedResources] != req.resource.UserData.ToMap()[LeasedResources] {
   375  		t.Errorf(
   376  			"resource user data from requirement %v should be the same as the one received %v",
   377  			req.resource.UserData.ToMap()[LeasedResources], res.UserData.ToMap()[LeasedResources])
   378  	}
   379  	if len(leasedResources) != 1 {
   380  		t.Errorf("there should be one leased resource, found %d", len(leasedResources))
   381  	}
   382  	if leasedResources[0] != leasedResource.Name {
   383  		t.Errorf("Leased resource don t match")
   384  	}
   385  }
   386  
   387  func TestMason(t *testing.T) {
   388  	count := 10
   389  	tc := testConfig{
   390  		"type1": {
   391  			count: count,
   392  		},
   393  		"type2": {
   394  			resourceNeeds: &common.ResourceNeeds{
   395  				"type1": 1,
   396  			},
   397  			count: count,
   398  		},
   399  	}
   400  	rStorage, mClient, configs, releasedResources := createFakeBoskos(tc)
   401  	m := NewMason(10, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   402  	m.storage.SyncConfigs(configs)
   403  	m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter)
   404  	m.Start()
   405  	timeout := time.NewTicker(5 * time.Second).C
   406  	i := 0
   407  loop:
   408  	for {
   409  		select {
   410  		case <-timeout:
   411  			t.Errorf("Test timed ouf")
   412  			t.FailNow()
   413  		case rr := <-releasedResources:
   414  			if strings.HasPrefix(rr.name, "type2_") {
   415  				if rr.state != common.Free {
   416  					t.Errorf("resource %s should be at state %s, found %s", rr.name, common.Free, rr.state)
   417  				}
   418  			} else if strings.HasPrefix(rr.name, "type1_") {
   419  				if !strings.HasPrefix(rr.state, "type2_") {
   420  					t.Errorf("resource %s should be starting with type2_, found %s", rr.name, rr.state)
   421  				}
   422  			}
   423  			i++
   424  			if i >= 2*count {
   425  				break loop
   426  			}
   427  		}
   428  	}
   429  
   430  	leasedResourceFromRes := func(r common.Resource) (l []common.Resource) {
   431  		var leasedResources []string
   432  		r.UserData.Extract(LeasedResources, &leasedResources)
   433  		for _, name := range leasedResources {
   434  			r, _ := rStorage.GetResource(name)
   435  			l = append(l, r)
   436  		}
   437  		return
   438  	}
   439  
   440  	var resourcesToRelease []common.Resource
   441  
   442  	for i := 0; i < 10; i++ {
   443  		res, err := mClient.Acquire("type2", common.Free, "Used")
   444  		if err != nil {
   445  			t.Errorf("Count %d: There should be free resources", i)
   446  			t.FailNow()
   447  		}
   448  		leasedResources := leasedResourceFromRes(*res)
   449  		if len(leasedResources) != 1 {
   450  			t.Error("there should be 1 resource of type1")
   451  		}
   452  		for _, r := range leasedResources {
   453  			if r.Type != "type1" {
   454  				t.Error("resource should be of type type1")
   455  			}
   456  		}
   457  		resourcesToRelease = append(resourcesToRelease, *res)
   458  	}
   459  	if _, err := mClient.Acquire("type2", common.Free, "Used"); err == nil {
   460  		t.Error("there should not be any resource left")
   461  	}
   462  	m.Stop()
   463  	for _, res := range resourcesToRelease {
   464  		if err := mClient.ReleaseOne(res.Name, common.Dirty); err != nil {
   465  			t.Error("unable to release leased resources")
   466  
   467  		}
   468  	}
   469  	resources, _ := rStorage.GetResources()
   470  	for _, r := range resources {
   471  		if r.State != common.Dirty {
   472  			t.Errorf("state should be %s, found %s", common.Dirty, r.State)
   473  		}
   474  	}
   475  }
   476  
   477  func TestMasonStartStop(t *testing.T) {
   478  	tc := testConfig{
   479  		"type1": {
   480  			count: 10,
   481  		},
   482  		"type2": {
   483  			resourceNeeds: &common.ResourceNeeds{
   484  				"type1": 1,
   485  			},
   486  			count: 10,
   487  		},
   488  	}
   489  	_, mClient, configs, _ := createFakeBoskos(tc)
   490  	m := NewMason(5, mClient.basic, defaultWaitPeriod, defaultWaitPeriod)
   491  	m.storage.SyncConfigs(configs)
   492  	m.RegisterConfigConverter(fakeConfigType, failingConfigConverter)
   493  	m.Start()
   494  	done := make(chan bool)
   495  	go func() {
   496  		m.Stop()
   497  		done <- true
   498  	}()
   499  	select {
   500  	case <-time.After(time.Second):
   501  		t.Errorf("unable to stop mason")
   502  	case <-done:
   503  	}
   504  }
   505  
   506  func TestConfig(t *testing.T) {
   507  	resources, err := ranch.ParseConfig("test-resources.yaml")
   508  	if err != nil {
   509  		t.Error(err)
   510  	}
   511  	configs, err := ParseConfig("test-configs.yaml")
   512  	if err != nil {
   513  		t.Error(err)
   514  	}
   515  	if err := ValidateConfig(configs, resources); err == nil {
   516  		t.Error(err)
   517  	}
   518  }
   519  
   520  func makeFakeConfig(name, cType, content string, needs int) common.ResourcesConfig {
   521  	c := common.ResourcesConfig{
   522  		Name:  name,
   523  		Needs: common.ResourceNeeds{},
   524  		Config: common.ConfigType{
   525  			Type:    cType,
   526  			Content: content,
   527  		},
   528  	}
   529  	for i := 0; i < needs; i++ {
   530  		c.Needs[fmt.Sprintf("type_%d", i)] = i
   531  	}
   532  	return c
   533  }
   534  
   535  func TestSyncConfig(t *testing.T) {
   536  	var testcases = []struct {
   537  		name      string
   538  		oldConfig []common.ResourcesConfig
   539  		newConfig []common.ResourcesConfig
   540  		expect    []common.ResourcesConfig
   541  	}{
   542  		{
   543  			name: "empty",
   544  		},
   545  		{
   546  			name: "deleteAll",
   547  			oldConfig: []common.ResourcesConfig{
   548  				makeFakeConfig("config1", "fakeType", "", 2),
   549  				makeFakeConfig("config2", "fakeType", "", 3),
   550  				makeFakeConfig("config3", "fakeType", "", 4),
   551  			},
   552  		},
   553  		{
   554  			name: "new",
   555  			newConfig: []common.ResourcesConfig{
   556  				makeFakeConfig("config1", "fakeType", "", 2),
   557  				makeFakeConfig("config2", "fakeType", "", 3),
   558  				makeFakeConfig("config3", "fakeType", "", 4),
   559  			},
   560  			expect: []common.ResourcesConfig{
   561  				makeFakeConfig("config1", "fakeType", "", 2),
   562  				makeFakeConfig("config2", "fakeType", "", 3),
   563  				makeFakeConfig("config3", "fakeType", "", 4),
   564  			},
   565  		},
   566  		{
   567  			name: "noChange",
   568  			oldConfig: []common.ResourcesConfig{
   569  				makeFakeConfig("config1", "fakeType", "", 2),
   570  				makeFakeConfig("config2", "fakeType", "", 3),
   571  				makeFakeConfig("config3", "fakeType", "", 4),
   572  			},
   573  			newConfig: []common.ResourcesConfig{
   574  				makeFakeConfig("config1", "fakeType", "", 2),
   575  				makeFakeConfig("config2", "fakeType", "", 3),
   576  				makeFakeConfig("config3", "fakeType", "", 4),
   577  			},
   578  			expect: []common.ResourcesConfig{
   579  				makeFakeConfig("config1", "fakeType", "", 2),
   580  				makeFakeConfig("config2", "fakeType", "", 3),
   581  				makeFakeConfig("config3", "fakeType", "", 4),
   582  			},
   583  		},
   584  		{
   585  			name: "update",
   586  			oldConfig: []common.ResourcesConfig{
   587  				makeFakeConfig("config1", "fakeType", "", 2),
   588  				makeFakeConfig("config2", "fakeType", "", 3),
   589  				makeFakeConfig("config3", "fakeType", "", 4),
   590  			},
   591  			newConfig: []common.ResourcesConfig{
   592  				makeFakeConfig("config1", "fakeType2", "", 2),
   593  				makeFakeConfig("config2", "fakeType", "something", 3),
   594  				makeFakeConfig("config3", "fakeType", "", 5),
   595  			},
   596  			expect: []common.ResourcesConfig{
   597  				makeFakeConfig("config1", "fakeType2", "", 2),
   598  				makeFakeConfig("config2", "fakeType", "something", 3),
   599  				makeFakeConfig("config3", "fakeType", "", 5),
   600  			},
   601  		},
   602  		{
   603  			name: "delete",
   604  			oldConfig: []common.ResourcesConfig{
   605  				makeFakeConfig("config1", "fakeType", "", 2),
   606  				makeFakeConfig("config2", "fakeType", "", 3),
   607  				makeFakeConfig("config3", "fakeType", "", 4),
   608  			},
   609  			newConfig: []common.ResourcesConfig{
   610  				makeFakeConfig("config1", "fakeType2", "", 2),
   611  				makeFakeConfig("config3", "fakeType", "", 5),
   612  			},
   613  			expect: []common.ResourcesConfig{
   614  				makeFakeConfig("config1", "fakeType2", "", 2),
   615  				makeFakeConfig("config3", "fakeType", "", 5),
   616  			},
   617  		},
   618  	}
   619  
   620  	for _, tc := range testcases {
   621  		s := newStorage(storage.NewMemoryStorage())
   622  		s.SyncConfigs(tc.newConfig)
   623  		configs, err := s.GetConfigs()
   624  		if err != nil {
   625  			t.Errorf("failed to get resources")
   626  			continue
   627  		}
   628  		sort.Stable(common.ResourcesConfigByName(configs))
   629  		sort.Stable(common.ResourcesConfigByName(tc.expect))
   630  		if !reflect.DeepEqual(configs, tc.expect) {
   631  			t.Errorf("Test %v: got %v, expect %v", tc.name, configs, tc.expect)
   632  		}
   633  	}
   634  }
   635  
   636  func TestGetConfig(t *testing.T) {
   637  	var testcases = []struct {
   638  		name, configName string
   639  		exists           bool
   640  		configs          []common.ResourcesConfig
   641  	}{
   642  		{
   643  			name:       "exists",
   644  			exists:     true,
   645  			configName: "test",
   646  			configs: []common.ResourcesConfig{
   647  				{
   648  					Needs: common.ResourceNeeds{"type1": 1, "type2": 2},
   649  					Config: common.ConfigType{
   650  						Type:    "type3",
   651  						Content: "content",
   652  					},
   653  					Name: "test",
   654  				},
   655  			},
   656  		},
   657  		{
   658  			name:       "noConfig",
   659  			exists:     false,
   660  			configName: "test",
   661  		},
   662  		{
   663  			name:       "existsMultipleConfigs",
   664  			exists:     true,
   665  			configName: "test1",
   666  			configs: []common.ResourcesConfig{
   667  				{
   668  					Needs: common.ResourceNeeds{"type1": 1, "type2": 2},
   669  					Config: common.ConfigType{
   670  						Type:    "type3",
   671  						Content: "content",
   672  					},
   673  					Name: "test",
   674  				},
   675  				{
   676  					Needs: common.ResourceNeeds{"type1": 1, "type2": 2},
   677  					Config: common.ConfigType{
   678  						Type:    "type3",
   679  						Content: "content",
   680  					},
   681  					Name: "test1",
   682  				},
   683  			},
   684  		},
   685  		{
   686  			name:       "noExistMultipleConfigs",
   687  			exists:     false,
   688  			configName: "test2",
   689  			configs: []common.ResourcesConfig{
   690  				{
   691  					Needs: common.ResourceNeeds{"type1": 1, "type2": 2},
   692  					Config: common.ConfigType{
   693  						Type:    "type3",
   694  						Content: "content",
   695  					},
   696  					Name: "test",
   697  				},
   698  				{
   699  					Needs: common.ResourceNeeds{"type1": 1, "type2": 2},
   700  					Config: common.ConfigType{
   701  						Type:    "type3",
   702  						Content: "content",
   703  					},
   704  					Name: "test1",
   705  				},
   706  			},
   707  		},
   708  	}
   709  	for _, tc := range testcases {
   710  		s := newStorage(storage.NewMemoryStorage())
   711  		for _, config := range tc.configs {
   712  			s.AddConfig(config)
   713  		}
   714  		config, err := s.GetConfig(tc.configName)
   715  		if !tc.exists {
   716  			if err == nil {
   717  				t.Error("client should return an error")
   718  			}
   719  		} else {
   720  			if config.Name != tc.configName {
   721  				t.Error("config name should match")
   722  			}
   723  		}
   724  	}
   725  }