github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/juju/apiconn_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package juju_test
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"time"
    10  
    11  	gc "launchpad.net/gocheck"
    12  
    13  	"launchpad.net/juju-core/constraints"
    14  	"launchpad.net/juju-core/environs"
    15  	"launchpad.net/juju-core/environs/bootstrap"
    16  	"launchpad.net/juju-core/environs/config"
    17  	"launchpad.net/juju-core/environs/configstore"
    18  	envtesting "launchpad.net/juju-core/environs/testing"
    19  	"launchpad.net/juju-core/juju"
    20  	"launchpad.net/juju-core/juju/osenv"
    21  	"launchpad.net/juju-core/provider/dummy"
    22  	"launchpad.net/juju-core/state/api"
    23  	coretesting "launchpad.net/juju-core/testing"
    24  	jc "launchpad.net/juju-core/testing/checkers"
    25  	"launchpad.net/juju-core/testing/testbase"
    26  )
    27  
    28  type NewAPIConnSuite struct {
    29  	testbase.LoggingSuite
    30  	envtesting.ToolsFixture
    31  }
    32  
    33  var _ = gc.Suite(&NewAPIConnSuite{})
    34  
    35  func (cs *NewAPIConnSuite) SetUpTest(c *gc.C) {
    36  	cs.LoggingSuite.SetUpTest(c)
    37  	cs.ToolsFixture.SetUpTest(c)
    38  }
    39  
    40  func (cs *NewAPIConnSuite) TearDownTest(c *gc.C) {
    41  	dummy.Reset()
    42  	cs.ToolsFixture.TearDownTest(c)
    43  	cs.LoggingSuite.TearDownTest(c)
    44  }
    45  
    46  func (*NewAPIConnSuite) TestNewConn(c *gc.C) {
    47  	cfg, err := config.New(config.NoDefaults, dummy.SampleConfig())
    48  	c.Assert(err, gc.IsNil)
    49  	ctx := coretesting.Context(c)
    50  	env, err := environs.Prepare(cfg, ctx, configstore.NewMem())
    51  	c.Assert(err, gc.IsNil)
    52  
    53  	envtesting.UploadFakeTools(c, env.Storage())
    54  	err = bootstrap.Bootstrap(ctx, env, constraints.Value{})
    55  	c.Assert(err, gc.IsNil)
    56  
    57  	cfg = env.Config()
    58  	cfg, err = cfg.Apply(map[string]interface{}{
    59  		"secret": "fnord",
    60  	})
    61  	c.Assert(err, gc.IsNil)
    62  	err = env.SetConfig(cfg)
    63  	c.Assert(err, gc.IsNil)
    64  
    65  	conn, err := juju.NewAPIConn(env, api.DefaultDialOpts())
    66  	c.Assert(err, gc.IsNil)
    67  	c.Assert(conn.Environ, gc.Equals, env)
    68  	c.Assert(conn.State, gc.NotNil)
    69  
    70  	// the secrets will not be updated, as they already exist
    71  	attrs, err := conn.State.Client().EnvironmentGet()
    72  	c.Assert(attrs["secret"], gc.Equals, "pork")
    73  
    74  	c.Assert(conn.Close(), gc.IsNil)
    75  }
    76  
    77  type NewAPIClientSuite struct {
    78  	testbase.LoggingSuite
    79  }
    80  
    81  var _ = gc.Suite(&NewAPIClientSuite{})
    82  
    83  func (cs *NewAPIClientSuite) TearDownTest(c *gc.C) {
    84  	dummy.Reset()
    85  	cs.LoggingSuite.TearDownTest(c)
    86  }
    87  
    88  func (*NewAPIClientSuite) TestNameDefault(c *gc.C) {
    89  	defer coretesting.MakeMultipleEnvHome(c).Restore()
    90  	// The connection logic should not delay the config connection
    91  	// at all when there is no environment info available.
    92  	// Make sure of that by providing a suitably long delay
    93  	// and checking that the connection happens within that
    94  	// time.
    95  	defer testbase.PatchValue(juju.ProviderConnectDelay, coretesting.LongWait).Restore()
    96  	bootstrapEnv(c, coretesting.SampleEnvName, defaultConfigStore(c))
    97  
    98  	startTime := time.Now()
    99  	apiclient, err := juju.NewAPIClientFromName("")
   100  	c.Assert(err, gc.IsNil)
   101  	defer apiclient.Close()
   102  	c.Assert(time.Since(startTime), jc.LessThan, coretesting.LongWait)
   103  
   104  	// We should get the default sample environment if we ask for ""
   105  	assertEnvironmentName(c, apiclient, coretesting.SampleEnvName)
   106  }
   107  
   108  func (*NewAPIClientSuite) TestNameNotDefault(c *gc.C) {
   109  	defer coretesting.MakeMultipleEnvHome(c).Restore()
   110  	envName := coretesting.SampleCertName + "-2"
   111  	bootstrapEnv(c, envName, defaultConfigStore(c))
   112  	apiclient, err := juju.NewAPIClientFromName(envName)
   113  	c.Assert(err, gc.IsNil)
   114  	defer apiclient.Close()
   115  	assertEnvironmentName(c, apiclient, envName)
   116  }
   117  
   118  func (*NewAPIClientSuite) TestWithInfoOnly(c *gc.C) {
   119  	defer coretesting.MakeEmptyFakeHome(c).Restore()
   120  	storeConfig := &environInfo{
   121  		creds: configstore.APICredentials{
   122  			User:     "foo",
   123  			Password: "foopass",
   124  		},
   125  		endpoint: configstore.APIEndpoint{
   126  			Addresses: []string{"foo.invalid"},
   127  			CACert:    "certificated",
   128  		},
   129  	}
   130  	store := newConfigStore("noconfig", storeConfig)
   131  
   132  	called := 0
   133  	expectState := new(api.State)
   134  	apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
   135  		c.Check(apiInfo.Tag, gc.Equals, "user-foo")
   136  		c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
   137  		c.Check(apiInfo.Password, gc.Equals, "foopass")
   138  		c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
   139  		called++
   140  		return expectState, nil
   141  	}
   142  	// Give NewAPIFromName a store interface that can report when the
   143  	// config was written to, to ensure the cache isn't updated.
   144  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   145  	mockStore := &storageWithWriteNotify{store: store}
   146  	st, err := juju.NewAPIFromName("noconfig", mockStore)
   147  	c.Assert(err, gc.IsNil)
   148  	c.Assert(st, gc.Equals, expectState)
   149  	c.Assert(called, gc.Equals, 1)
   150  	c.Assert(mockStore.written, jc.IsFalse)
   151  }
   152  
   153  func (*NewAPIClientSuite) TestWithConfigAndNoInfo(c *gc.C) {
   154  	defer coretesting.MakeSampleHome(c).Restore()
   155  
   156  	store := newConfigStore(coretesting.SampleEnvName, &environInfo{
   157  		bootstrapConfig: map[string]interface{}{
   158  			"type":                      "dummy",
   159  			"name":                      "myenv",
   160  			"state-server":              true,
   161  			"authorized-keys":           "i-am-a-key",
   162  			"default-series":            config.DefaultSeries,
   163  			"firewall-mode":             config.FwInstance,
   164  			"development":               false,
   165  			"ssl-hostname-verification": true,
   166  			"admin-secret":              "adminpass",
   167  		},
   168  	})
   169  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   170  
   171  	// Verify the cache is empty.
   172  	info, err := store.ReadInfo("myenv")
   173  	c.Assert(err, gc.IsNil)
   174  	c.Assert(info, gc.NotNil)
   175  	c.Assert(info.APIEndpoint(), jc.DeepEquals, configstore.APIEndpoint{})
   176  	c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{})
   177  
   178  	called := 0
   179  	expectState := new(api.State)
   180  	apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
   181  		c.Check(apiInfo.Tag, gc.Equals, "user-admin")
   182  		c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "")
   183  		c.Check(apiInfo.Password, gc.Equals, "adminpass")
   184  		c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
   185  		called++
   186  		return expectState, nil
   187  	}
   188  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   189  	st, err := juju.NewAPIFromName("myenv", store)
   190  	c.Assert(err, gc.IsNil)
   191  	c.Assert(st, gc.Equals, expectState)
   192  	c.Assert(called, gc.Equals, 1)
   193  
   194  	// Make sure the cache is updated.
   195  	info, err = store.ReadInfo("myenv")
   196  	c.Assert(err, gc.IsNil)
   197  	c.Assert(info, gc.NotNil)
   198  	ep := info.APIEndpoint()
   199  	c.Check(ep.Addresses, gc.HasLen, 1)
   200  	c.Check(ep.Addresses[0], gc.Matches, `127\.0\.0\.1:\d+`)
   201  	c.Check(ep.CACert, gc.Not(gc.Equals), "")
   202  	creds := info.APICredentials()
   203  	c.Check(creds.User, gc.Equals, "admin")
   204  	c.Check(creds.Password, gc.Equals, "adminpass")
   205  }
   206  
   207  func (*NewAPIClientSuite) TestWithInfoError(c *gc.C) {
   208  	defer coretesting.MakeEmptyFakeHome(c).Restore()
   209  	expectErr := fmt.Errorf("an error")
   210  	store := newConfigStoreWithError(expectErr)
   211  	defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()
   212  	client, err := juju.NewAPIFromName("noconfig", store)
   213  	c.Assert(err, gc.Equals, expectErr)
   214  	c.Assert(client, gc.IsNil)
   215  }
   216  
   217  func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
   218  	panic("api.Open called unexpectedly")
   219  }
   220  
   221  func (*NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) {
   222  	defer coretesting.MakeEmptyFakeHome(c).Restore()
   223  	store := newConfigStore("noconfig", &environInfo{
   224  		endpoint: configstore.APIEndpoint{
   225  			Addresses: []string{},
   226  			CACert:    "certificated",
   227  		},
   228  	})
   229  	defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore()
   230  
   231  	st, err := juju.NewAPIFromName("noconfig", store)
   232  	c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)
   233  	c.Assert(st, gc.IsNil)
   234  }
   235  
   236  func (*NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) {
   237  	defer coretesting.MakeEmptyFakeHome(c).Restore()
   238  	store := newConfigStore("noconfig", &environInfo{
   239  		endpoint: configstore.APIEndpoint{
   240  			Addresses: []string{"foo.invalid"},
   241  		},
   242  	})
   243  
   244  	expectErr := fmt.Errorf("an error")
   245  	apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
   246  		return nil, expectErr
   247  	}
   248  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   249  	st, err := juju.NewAPIFromName("noconfig", store)
   250  	c.Assert(err, gc.Equals, expectErr)
   251  	c.Assert(st, gc.IsNil)
   252  }
   253  
   254  func (*NewAPIClientSuite) TestWithSlowInfoConnect(c *gc.C) {
   255  	defer coretesting.MakeSampleHome(c).Restore()
   256  	store := configstore.NewMem()
   257  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   258  	setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
   259  
   260  	infoOpenedState := new(api.State)
   261  	infoEndpointOpened := make(chan struct{})
   262  	cfgOpenedState := new(api.State)
   263  	// On a sample run with no delay, the logic took 45ms to run, so
   264  	// we make the delay slightly more than that, so that if the
   265  	// logic doesn't delay at all, the test will fail reasonably consistently.
   266  	defer testbase.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond).Restore()
   267  	apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
   268  		if info.Addrs[0] == "infoapi.invalid" {
   269  			infoEndpointOpened <- struct{}{}
   270  			return infoOpenedState, nil
   271  		}
   272  		return cfgOpenedState, nil
   273  	}
   274  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   275  
   276  	stateClosed, restoreAPIClose := setAPIClosed()
   277  	defer restoreAPIClose.Restore()
   278  
   279  	startTime := time.Now()
   280  	st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)
   281  	c.Assert(err, gc.IsNil)
   282  	// The connection logic should wait for some time before opening
   283  	// the API from the configuration.
   284  	c.Assert(time.Since(startTime), jc.GreaterThan, *juju.ProviderConnectDelay)
   285  	c.Assert(st, gc.Equals, cfgOpenedState)
   286  
   287  	select {
   288  	case <-infoEndpointOpened:
   289  	case <-time.After(coretesting.LongWait):
   290  		c.Errorf("api never opened via info")
   291  	}
   292  
   293  	// Check that the ignored state was closed.
   294  	select {
   295  	case st := <-stateClosed:
   296  		c.Assert(st, gc.Equals, infoOpenedState)
   297  	case <-time.After(coretesting.LongWait):
   298  		c.Errorf("timed out waiting for state to be closed")
   299  	}
   300  }
   301  
   302  func setEndpointAddress(c *gc.C, store configstore.Storage, envName string, addr string) {
   303  	// Populate the environment's info with an endpoint
   304  	// with a known address.
   305  	info, err := store.ReadInfo(coretesting.SampleEnvName)
   306  	c.Assert(err, gc.IsNil)
   307  	info.SetAPIEndpoint(configstore.APIEndpoint{
   308  		Addresses: []string{addr},
   309  		CACert:    "certificated",
   310  	})
   311  	err = info.Write()
   312  	c.Assert(err, gc.IsNil)
   313  }
   314  
   315  func (*NewAPIClientSuite) TestWithSlowConfigConnect(c *gc.C) {
   316  	defer coretesting.MakeSampleHome(c).Restore()
   317  
   318  	store := configstore.NewMem()
   319  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   320  	setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
   321  
   322  	infoOpenedState := new(api.State)
   323  	infoEndpointOpened := make(chan struct{})
   324  	cfgOpenedState := new(api.State)
   325  	cfgEndpointOpened := make(chan struct{})
   326  
   327  	defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore()
   328  	apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
   329  		if info.Addrs[0] == "infoapi.invalid" {
   330  			infoEndpointOpened <- struct{}{}
   331  			<-infoEndpointOpened
   332  			return infoOpenedState, nil
   333  		}
   334  		cfgEndpointOpened <- struct{}{}
   335  		<-cfgEndpointOpened
   336  		return cfgOpenedState, nil
   337  	}
   338  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   339  
   340  	stateClosed, restoreAPIClose := setAPIClosed()
   341  	defer restoreAPIClose.Restore()
   342  
   343  	done := make(chan struct{})
   344  	go func() {
   345  		st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)
   346  		c.Check(err, gc.IsNil)
   347  		c.Check(st, gc.Equals, infoOpenedState)
   348  		close(done)
   349  	}()
   350  
   351  	// Check that we're trying to connect to both endpoints:
   352  	select {
   353  	case <-infoEndpointOpened:
   354  	case <-time.After(coretesting.LongWait):
   355  		c.Fatalf("api never opened via info")
   356  	}
   357  	select {
   358  	case <-cfgEndpointOpened:
   359  	case <-time.After(coretesting.LongWait):
   360  		c.Fatalf("api never opened via config")
   361  	}
   362  	// Let the info endpoint open go ahead and
   363  	// check that the NewAPIFromName call returns.
   364  	infoEndpointOpened <- struct{}{}
   365  	select {
   366  	case <-done:
   367  	case <-time.After(coretesting.LongWait):
   368  		c.Errorf("timed out opening API")
   369  	}
   370  
   371  	// Let the config endpoint open go ahead and
   372  	// check that its state is closed.
   373  	cfgEndpointOpened <- struct{}{}
   374  	select {
   375  	case st := <-stateClosed:
   376  		c.Assert(st, gc.Equals, cfgOpenedState)
   377  	case <-time.After(coretesting.LongWait):
   378  		c.Errorf("timed out waiting for state to be closed")
   379  	}
   380  }
   381  
   382  func (*NewAPIClientSuite) TestBothError(c *gc.C) {
   383  	defer coretesting.MakeSampleHome(c).Restore()
   384  	store := configstore.NewMem()
   385  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   386  	setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
   387  
   388  	defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore()
   389  	apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {
   390  		if info.Addrs[0] == "infoapi.invalid" {
   391  			return nil, fmt.Errorf("info connect failed")
   392  		}
   393  		return nil, fmt.Errorf("config connect failed")
   394  	}
   395  	defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore()
   396  	st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)
   397  	c.Check(err, gc.ErrorMatches, "config connect failed")
   398  	c.Check(st, gc.IsNil)
   399  }
   400  
   401  func defaultConfigStore(c *gc.C) configstore.Storage {
   402  	store, err := configstore.Default()
   403  	c.Assert(err, gc.IsNil)
   404  	return store
   405  }
   406  
   407  // TODO(jam): 2013-08-27 This should move somewhere in api.*
   408  func (*NewAPIClientSuite) TestMultipleCloseOk(c *gc.C) {
   409  	defer coretesting.MakeSampleHome(c).Restore()
   410  	bootstrapEnv(c, "", defaultConfigStore(c))
   411  	client, _ := juju.NewAPIClientFromName("")
   412  	c.Assert(client.Close(), gc.IsNil)
   413  	c.Assert(client.Close(), gc.IsNil)
   414  	c.Assert(client.Close(), gc.IsNil)
   415  }
   416  
   417  func (*NewAPIClientSuite) TestWithBootstrapConfigAndNoEnvironmentsFile(c *gc.C) {
   418  	defer coretesting.MakeSampleHome(c).Restore()
   419  	store := configstore.NewMem()
   420  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   421  	info, err := store.ReadInfo(coretesting.SampleEnvName)
   422  	c.Assert(err, gc.IsNil)
   423  	c.Assert(info.BootstrapConfig(), gc.NotNil)
   424  	c.Assert(info.APIEndpoint().Addresses, gc.HasLen, 0)
   425  
   426  	err = os.Remove(osenv.JujuHomePath("environments.yaml"))
   427  	c.Assert(err, gc.IsNil)
   428  
   429  	st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store)
   430  	c.Check(err, gc.IsNil)
   431  	st.Close()
   432  }
   433  
   434  func (*NewAPIClientSuite) TestWithBootstrapConfigTakesPrecedence(c *gc.C) {
   435  	// We want to make sure that the code is using the bootstrap
   436  	// config rather than information from environments.yaml,
   437  	// even when there is an entry in environments.yaml
   438  	// We can do that by changing the info bootstrap config
   439  	// so it has a different environment name.
   440  	defer coretesting.MakeMultipleEnvHome(c).Restore()
   441  
   442  	store := configstore.NewMem()
   443  	bootstrapEnv(c, coretesting.SampleEnvName, store)
   444  	info, err := store.ReadInfo(coretesting.SampleEnvName)
   445  	c.Assert(err, gc.IsNil)
   446  
   447  	envName2 := coretesting.SampleCertName + "-2"
   448  	info2, err := store.CreateInfo(envName2)
   449  	c.Assert(err, gc.IsNil)
   450  	info2.SetBootstrapConfig(info.BootstrapConfig())
   451  	err = info2.Write()
   452  	c.Assert(err, gc.IsNil)
   453  
   454  	// Now we have info for envName2 which will actually
   455  	// cause a connection to the originally bootstrapped
   456  	// state.
   457  	st, err := juju.NewAPIFromName(envName2, store)
   458  	c.Check(err, gc.IsNil)
   459  	st.Close()
   460  
   461  	// Sanity check that connecting to the envName2
   462  	// but with no info fails.
   463  	// Currently this panics with an "environment not prepared" error.
   464  	// Disable for now until an upcoming branch fixes it.
   465  	//	err = info2.Destroy()
   466  	//	c.Assert(err, gc.IsNil)
   467  	//	st, err = juju.NewAPIFromName(envName2, store)
   468  	//	if err == nil {
   469  	//		st.Close()
   470  	//	}
   471  	//	c.Assert(err, gc.ErrorMatches, "fooobie")
   472  }
   473  
   474  func assertEnvironmentName(c *gc.C, client *api.Client, expectName string) {
   475  	envInfo, err := client.EnvironmentInfo()
   476  	c.Assert(err, gc.IsNil)
   477  	c.Assert(envInfo.Name, gc.Equals, expectName)
   478  }
   479  
   480  func setAPIClosed() (<-chan *api.State, testbase.Restorer) {
   481  	stateClosed := make(chan *api.State)
   482  	apiClose := func(st *api.State) error {
   483  		stateClosed <- st
   484  		return nil
   485  	}
   486  	return stateClosed, testbase.PatchValue(juju.APIClose, apiClose)
   487  }
   488  
   489  func updateSecretsNoop(_ environs.Environ, _ *api.State) error {
   490  	return nil
   491  }
   492  
   493  // newConfigStoreWithError that will return the given
   494  // error from ReadInfo.
   495  func newConfigStoreWithError(err error) configstore.Storage {
   496  	return &errorConfigStorage{
   497  		Storage: configstore.NewMem(),
   498  		err:     err,
   499  	}
   500  }
   501  
   502  type errorConfigStorage struct {
   503  	configstore.Storage
   504  	err error
   505  }
   506  
   507  func (store *errorConfigStorage) ReadInfo(envName string) (configstore.EnvironInfo, error) {
   508  	return nil, store.err
   509  }
   510  
   511  type environInfo struct {
   512  	creds           configstore.APICredentials
   513  	endpoint        configstore.APIEndpoint
   514  	bootstrapConfig map[string]interface{}
   515  }
   516  
   517  // newConfigStore returns a storage that contains information
   518  // for the environment name.
   519  func newConfigStore(envName string, info *environInfo) configstore.Storage {
   520  	store := configstore.NewMem()
   521  	newInfo, err := store.CreateInfo(envName)
   522  	if err != nil {
   523  		panic(err)
   524  	}
   525  	newInfo.SetAPICredentials(info.creds)
   526  	newInfo.SetAPIEndpoint(info.endpoint)
   527  	newInfo.SetBootstrapConfig(info.bootstrapConfig)
   528  	err = newInfo.Write()
   529  	if err != nil {
   530  		panic(err)
   531  	}
   532  	return store
   533  }
   534  
   535  type storageWithWriteNotify struct {
   536  	written bool
   537  	store   configstore.Storage
   538  }
   539  
   540  func (*storageWithWriteNotify) CreateInfo(envName string) (configstore.EnvironInfo, error) {
   541  	panic("CreateInfo not implemented")
   542  }
   543  
   544  func (s *storageWithWriteNotify) ReadInfo(envName string) (configstore.EnvironInfo, error) {
   545  	info, err := s.store.ReadInfo(envName)
   546  	if err != nil {
   547  		return nil, err
   548  	}
   549  	return &infoWithWriteNotify{
   550  		written:     &s.written,
   551  		EnvironInfo: info,
   552  	}, nil
   553  }
   554  
   555  type infoWithWriteNotify struct {
   556  	configstore.EnvironInfo
   557  	written *bool
   558  }
   559  
   560  func (info *infoWithWriteNotify) Write() error {
   561  	*info.written = true
   562  	return info.EnvironInfo.Write()
   563  }