github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/apiserver/service/service_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package service_test
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sync"
    10  
    11  	"github.com/juju/errors"
    12  	jc "github.com/juju/testing/checkers"
    13  	"github.com/juju/utils"
    14  	gc "gopkg.in/check.v1"
    15  	"gopkg.in/juju/charm.v6-unstable"
    16  	"gopkg.in/juju/charmrepo.v1/csclient"
    17  	"gopkg.in/macaroon.v1"
    18  	"gopkg.in/mgo.v2"
    19  
    20  	commontesting "github.com/juju/juju/apiserver/common/testing"
    21  	"github.com/juju/juju/apiserver/params"
    22  	"github.com/juju/juju/apiserver/service"
    23  	apiservertesting "github.com/juju/juju/apiserver/testing"
    24  	"github.com/juju/juju/constraints"
    25  	"github.com/juju/juju/instance"
    26  	jujutesting "github.com/juju/juju/juju/testing"
    27  	"github.com/juju/juju/state"
    28  	statestorage "github.com/juju/juju/state/storage"
    29  	"github.com/juju/juju/storage"
    30  	"github.com/juju/juju/storage/poolmanager"
    31  	"github.com/juju/juju/storage/provider"
    32  	"github.com/juju/juju/storage/provider/registry"
    33  	"github.com/juju/juju/testcharms"
    34  	"github.com/juju/juju/testing/factory"
    35  )
    36  
    37  type serviceSuite struct {
    38  	jujutesting.JujuConnSuite
    39  	apiservertesting.CharmStoreSuite
    40  	commontesting.BlockHelper
    41  
    42  	serviceApi *service.API
    43  	service    *state.Service
    44  	authorizer apiservertesting.FakeAuthorizer
    45  }
    46  
    47  var _ = gc.Suite(&serviceSuite{})
    48  
    49  var _ service.Service = (*service.API)(nil)
    50  
    51  func (s *serviceSuite) SetUpSuite(c *gc.C) {
    52  	s.CharmStoreSuite.SetUpSuite(c)
    53  	s.JujuConnSuite.SetUpSuite(c)
    54  }
    55  
    56  func (s *serviceSuite) TearDownSuite(c *gc.C) {
    57  	s.CharmStoreSuite.TearDownSuite(c)
    58  	s.JujuConnSuite.TearDownSuite(c)
    59  }
    60  
    61  func (s *serviceSuite) SetUpTest(c *gc.C) {
    62  	s.JujuConnSuite.SetUpTest(c)
    63  	s.BlockHelper = commontesting.NewBlockHelper(s.APIState)
    64  	s.AddCleanup(func(*gc.C) { s.BlockHelper.Close() })
    65  
    66  	s.CharmStoreSuite.Session = s.JujuConnSuite.Session
    67  	s.CharmStoreSuite.SetUpTest(c)
    68  
    69  	s.service = s.Factory.MakeService(c, nil)
    70  
    71  	s.authorizer = apiservertesting.FakeAuthorizer{
    72  		Tag: s.AdminUserTag(c),
    73  	}
    74  	var err error
    75  	s.serviceApi, err = service.NewAPI(s.State, nil, s.authorizer)
    76  	c.Assert(err, jc.ErrorIsNil)
    77  }
    78  
    79  func (s *serviceSuite) TearDownTest(c *gc.C) {
    80  	s.CharmStoreSuite.TearDownTest(c)
    81  	s.JujuConnSuite.TearDownTest(c)
    82  }
    83  
    84  func (s *serviceSuite) TestSetMetricCredentials(c *gc.C) {
    85  	charm := s.Factory.MakeCharm(c, &factory.CharmParams{Name: "wordpress"})
    86  	wordpress := s.Factory.MakeService(c, &factory.ServiceParams{
    87  		Charm: charm,
    88  	})
    89  	tests := []struct {
    90  		about   string
    91  		args    params.ServiceMetricCredentials
    92  		results params.ErrorResults
    93  	}{
    94  		{
    95  			"test one argument and it passes",
    96  			params.ServiceMetricCredentials{[]params.ServiceMetricCredential{{
    97  				s.service.Name(),
    98  				[]byte("creds 1234"),
    99  			}}},
   100  			params.ErrorResults{[]params.ErrorResult{{Error: nil}}},
   101  		},
   102  		{
   103  			"test two arguments and both pass",
   104  			params.ServiceMetricCredentials{[]params.ServiceMetricCredential{
   105  				{
   106  					s.service.Name(),
   107  					[]byte("creds 1234"),
   108  				},
   109  				{
   110  					wordpress.Name(),
   111  					[]byte("creds 4567"),
   112  				},
   113  			}},
   114  			params.ErrorResults{[]params.ErrorResult{
   115  				{Error: nil},
   116  				{Error: nil},
   117  			}},
   118  		},
   119  		{
   120  			"test two arguments and second one fails",
   121  			params.ServiceMetricCredentials{[]params.ServiceMetricCredential{
   122  				{
   123  					s.service.Name(),
   124  					[]byte("creds 1234"),
   125  				},
   126  				{
   127  					"not-a-service",
   128  					[]byte("creds 4567"),
   129  				},
   130  			}},
   131  			params.ErrorResults{[]params.ErrorResult{
   132  				{Error: nil},
   133  				{Error: &params.Error{`service "not-a-service" not found`, "not found"}},
   134  			}},
   135  		},
   136  	}
   137  	for i, t := range tests {
   138  		c.Logf("Running test %d %v", i, t.about)
   139  		results, err := s.serviceApi.SetMetricCredentials(t.args)
   140  		c.Assert(err, jc.ErrorIsNil)
   141  		c.Assert(results.Results, gc.HasLen, len(t.results.Results))
   142  		c.Assert(results, gc.DeepEquals, t.results)
   143  
   144  		for i, a := range t.args.Creds {
   145  			if t.results.Results[i].Error == nil {
   146  				svc, err := s.State.Service(a.ServiceName)
   147  				c.Assert(err, jc.ErrorIsNil)
   148  				creds := svc.MetricCredentials()
   149  				c.Assert(creds, gc.DeepEquals, a.MetricCredentials)
   150  			}
   151  		}
   152  	}
   153  }
   154  
   155  func (s *serviceSuite) TestCompatibleSettingsParsing(c *gc.C) {
   156  	// Test the exported settings parsing in a compatible way.
   157  	s.AddTestingService(c, "dummy", s.AddTestingCharm(c, "dummy"))
   158  	svc, err := s.State.Service("dummy")
   159  	c.Assert(err, jc.ErrorIsNil)
   160  	ch, _, err := svc.Charm()
   161  	c.Assert(err, jc.ErrorIsNil)
   162  	c.Assert(ch.URL().String(), gc.Equals, "local:quantal/dummy-1")
   163  
   164  	// Empty string will be returned as nil.
   165  	options := map[string]string{
   166  		"title":    "foobar",
   167  		"username": "",
   168  	}
   169  	settings, err := service.ParseSettingsCompatible(ch, options)
   170  	c.Assert(err, jc.ErrorIsNil)
   171  	c.Assert(settings, gc.DeepEquals, charm.Settings{
   172  		"title":    "foobar",
   173  		"username": nil,
   174  	})
   175  
   176  	// Illegal settings lead to an error.
   177  	options = map[string]string{
   178  		"yummy": "didgeridoo",
   179  	}
   180  	_, err = service.ParseSettingsCompatible(ch, options)
   181  	c.Assert(err, gc.ErrorMatches, `unknown option "yummy"`)
   182  }
   183  
   184  func setupStoragePool(c *gc.C, st *state.State) {
   185  	pm := poolmanager.New(state.NewStateSettings(st))
   186  	_, err := pm.Create("loop-pool", provider.LoopProviderType, map[string]interface{}{})
   187  	c.Assert(err, jc.ErrorIsNil)
   188  	err = st.UpdateEnvironConfig(map[string]interface{}{
   189  		"storage-default-block-source": "loop-pool",
   190  	}, nil, nil)
   191  	c.Assert(err, jc.ErrorIsNil)
   192  }
   193  
   194  func (s *serviceSuite) TestClientServiceDeployWithStorage(c *gc.C) {
   195  	setupStoragePool(c, s.State)
   196  	curl, ch := s.UploadCharm(c, "utopic/storage-block-10", "storage-block")
   197  	storageConstraints := map[string]storage.Constraints{
   198  		"data": {
   199  			Count: 1,
   200  			Size:  1024,
   201  			Pool:  "loop-pool",
   202  		},
   203  	}
   204  
   205  	var cons constraints.Value
   206  	args := params.ServiceDeploy{
   207  		ServiceName: "service",
   208  		CharmUrl:    curl.String(),
   209  		NumUnits:    1,
   210  		Constraints: cons,
   211  		Storage:     storageConstraints,
   212  	}
   213  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   214  		Services: []params.ServiceDeploy{args}},
   215  	)
   216  	c.Assert(err, jc.ErrorIsNil)
   217  	c.Assert(results, gc.DeepEquals, params.ErrorResults{
   218  		Results: []params.ErrorResult{{Error: nil}},
   219  	})
   220  	svc := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons)
   221  	storageConstraintsOut, err := svc.StorageConstraints()
   222  	c.Assert(err, jc.ErrorIsNil)
   223  	c.Assert(storageConstraintsOut, gc.DeepEquals, map[string]state.StorageConstraints{
   224  		"data": {
   225  			Count: 1,
   226  			Size:  1024,
   227  			Pool:  "loop-pool",
   228  		},
   229  		"allecto": {
   230  			Count: 0,
   231  			Size:  1024,
   232  			Pool:  "loop",
   233  		},
   234  	})
   235  }
   236  
   237  func (s *serviceSuite) TestClientServiceDeployWithInvalidStoragePool(c *gc.C) {
   238  	setupStoragePool(c, s.State)
   239  	curl, _ := s.UploadCharm(c, "utopic/storage-block-0", "storage-block")
   240  	storageConstraints := map[string]storage.Constraints{
   241  		"data": storage.Constraints{
   242  			Pool:  "foo",
   243  			Count: 1,
   244  			Size:  1024,
   245  		},
   246  	}
   247  
   248  	var cons constraints.Value
   249  	args := params.ServiceDeploy{
   250  		ServiceName: "service",
   251  		CharmUrl:    curl.String(),
   252  		NumUnits:    1,
   253  		Constraints: cons,
   254  		Storage:     storageConstraints,
   255  	}
   256  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   257  		Services: []params.ServiceDeploy{args}},
   258  	)
   259  	c.Assert(err, jc.ErrorIsNil)
   260  	c.Assert(results.Results, gc.HasLen, 1)
   261  	c.Assert(results.Results[0].Error, gc.ErrorMatches, `.* pool "foo" not found`)
   262  }
   263  
   264  func (s *serviceSuite) TestClientServiceDeployWithUnsupportedStoragePool(c *gc.C) {
   265  	registry.RegisterProvider("hostloop", &mockStorageProvider{kind: storage.StorageKindBlock})
   266  	pm := poolmanager.New(state.NewStateSettings(s.State))
   267  	_, err := pm.Create("host-loop-pool", provider.HostLoopProviderType, map[string]interface{}{})
   268  	c.Assert(err, jc.ErrorIsNil)
   269  
   270  	curl, _ := s.UploadCharm(c, "utopic/storage-block-0", "storage-block")
   271  	storageConstraints := map[string]storage.Constraints{
   272  		"data": storage.Constraints{
   273  			Pool:  "host-loop-pool",
   274  			Count: 1,
   275  			Size:  1024,
   276  		},
   277  	}
   278  
   279  	var cons constraints.Value
   280  	args := params.ServiceDeploy{
   281  		ServiceName: "service",
   282  		CharmUrl:    curl.String(),
   283  		NumUnits:    1,
   284  		Constraints: cons,
   285  		Storage:     storageConstraints,
   286  	}
   287  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   288  		Services: []params.ServiceDeploy{args}},
   289  	)
   290  	c.Assert(err, jc.ErrorIsNil)
   291  	c.Assert(results.Results, gc.HasLen, 1)
   292  	c.Assert(results.Results[0].Error, gc.ErrorMatches,
   293  		`.*pool "host-loop-pool" uses storage provider "hostloop" which is not supported for environments of type "dummy"`)
   294  }
   295  
   296  func (s *serviceSuite) TestClientServiceDeployDefaultFilesystemStorage(c *gc.C) {
   297  	setupStoragePool(c, s.State)
   298  	curl, ch := s.UploadCharm(c, "trusty/storage-filesystem-1", "storage-filesystem")
   299  	var cons constraints.Value
   300  	args := params.ServiceDeploy{
   301  		ServiceName: "service",
   302  		CharmUrl:    curl.String(),
   303  		NumUnits:    1,
   304  		Constraints: cons,
   305  	}
   306  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   307  		Services: []params.ServiceDeploy{args}},
   308  	)
   309  	c.Assert(err, jc.ErrorIsNil)
   310  	c.Assert(results, gc.DeepEquals, params.ErrorResults{
   311  		Results: []params.ErrorResult{{Error: nil}},
   312  	})
   313  	svc := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons)
   314  	storageConstraintsOut, err := svc.StorageConstraints()
   315  	c.Assert(err, jc.ErrorIsNil)
   316  	c.Assert(storageConstraintsOut, gc.DeepEquals, map[string]state.StorageConstraints{
   317  		"data": {
   318  			Count: 1,
   319  			Size:  1024,
   320  			Pool:  "rootfs",
   321  		},
   322  	})
   323  }
   324  
   325  func (s *serviceSuite) TestClientServiceDeployWithPlacement(c *gc.C) {
   326  	curl, ch := s.UploadCharm(c, "precise/dummy-42", "dummy")
   327  	err := service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()})
   328  	c.Assert(err, jc.ErrorIsNil)
   329  	var cons constraints.Value
   330  	args := params.ServiceDeploy{
   331  		ServiceName: "service",
   332  		CharmUrl:    curl.String(),
   333  		NumUnits:    1,
   334  		Constraints: cons,
   335  		Placement: []*instance.Placement{
   336  			{"deadbeef-0bad-400d-8000-4b1d0d06f00d", "valid"},
   337  		},
   338  		ToMachineSpec: "will be ignored",
   339  	}
   340  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   341  		Services: []params.ServiceDeploy{args}},
   342  	)
   343  	c.Assert(err, jc.ErrorIsNil)
   344  	c.Assert(results, gc.DeepEquals, params.ErrorResults{
   345  		Results: []params.ErrorResult{{Error: nil}},
   346  	})
   347  	svc := apiservertesting.AssertPrincipalServiceDeployed(c, s.State, "service", curl, false, ch, cons)
   348  	units, err := svc.AllUnits()
   349  	c.Assert(err, jc.ErrorIsNil)
   350  	c.Assert(units, gc.HasLen, 1)
   351  }
   352  
   353  func (s *serviceSuite) TestClientServiceDeployWithInvalidPlacement(c *gc.C) {
   354  	curl, _ := s.UploadCharm(c, "precise/dummy-42", "dummy")
   355  	err := service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()})
   356  	c.Assert(err, jc.ErrorIsNil)
   357  	var cons constraints.Value
   358  	args := params.ServiceDeploy{
   359  		ServiceName: "service",
   360  		CharmUrl:    curl.String(),
   361  		NumUnits:    1,
   362  		Constraints: cons,
   363  		Placement: []*instance.Placement{
   364  			{"deadbeef-0bad-400d-8000-4b1d0d06f00d", "invalid"},
   365  		},
   366  	}
   367  	results, err := s.serviceApi.ServicesDeploy(params.ServicesDeploy{
   368  		Services: []params.ServiceDeploy{args}},
   369  	)
   370  	c.Assert(err, jc.ErrorIsNil)
   371  	c.Assert(results.Results, gc.HasLen, 1)
   372  	c.Assert(results.Results[0].Error.Error(), gc.Matches, ".* invalid placement is invalid")
   373  }
   374  
   375  // TODO(wallyworld) - the following charm tests have been moved from the apiserver/client
   376  // package in order to use the fake charm store testing infrastructure. They are legacy tests
   377  // written to use the api client instead of the apiserver logic. They need to be rewritten and
   378  // feature tests added.
   379  
   380  func (s *serviceSuite) TestAddCharm(c *gc.C) {
   381  	var blobs blobs
   382  	s.PatchValue(service.NewStateStorage, func(uuid string, session *mgo.Session) statestorage.Storage {
   383  		storage := statestorage.NewStorage(uuid, session)
   384  		return &recordingStorage{Storage: storage, blobs: &blobs}
   385  	})
   386  
   387  	client := s.APIState.Client()
   388  	// First test the sanity checks.
   389  	err := client.AddCharm(&charm.URL{Name: "nonsense"})
   390  	c.Assert(err, gc.ErrorMatches, `cannot parse charm or bundle URL: ":nonsense-0"`)
   391  	err = client.AddCharm(charm.MustParseURL("local:precise/dummy"))
   392  	c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema")
   393  	err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress"))
   394  	c.Assert(err, gc.ErrorMatches, "charm URL must include revision")
   395  
   396  	// Add a charm, without uploading it to storage, to
   397  	// check that AddCharm does not try to do it.
   398  	charmDir := testcharms.Repo.CharmDir("dummy")
   399  	ident := fmt.Sprintf("%s-%d", charmDir.Meta().Name, charmDir.Revision())
   400  	curl := charm.MustParseURL("cs:quantal/" + ident)
   401  	sch, err := s.State.AddCharm(charmDir, curl, "", ident+"-sha256")
   402  	c.Assert(err, jc.ErrorIsNil)
   403  
   404  	// AddCharm should see the charm in state and not upload it.
   405  	err = client.AddCharm(sch.URL())
   406  	c.Assert(err, jc.ErrorIsNil)
   407  
   408  	c.Assert(blobs.m, gc.HasLen, 0)
   409  
   410  	// Now try adding another charm completely.
   411  	curl, _ = s.UploadCharm(c, "precise/wordpress-3", "wordpress")
   412  	err = client.AddCharm(curl)
   413  	c.Assert(err, jc.ErrorIsNil)
   414  
   415  	// Verify it's in state and it got uploaded.
   416  	storage := statestorage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession())
   417  	sch, err = s.State.Charm(curl)
   418  	c.Assert(err, jc.ErrorIsNil)
   419  	s.assertUploaded(c, storage, sch.StoragePath(), sch.BundleSha256())
   420  }
   421  
   422  func (s *serviceSuite) TestAddCharmWithAuthorization(c *gc.C) {
   423  	// Upload a new charm to the charm store.
   424  	curl, _ := s.UploadCharm(c, "cs:~restricted/precise/wordpress-3", "wordpress")
   425  
   426  	// Change permissions on the new charm such that only bob
   427  	// can read from it.
   428  	s.DischargeUser = "restricted"
   429  	err := s.Client.Put("/"+curl.Path()+"/meta/perm/read", []string{"bob"})
   430  	c.Assert(err, jc.ErrorIsNil)
   431  
   432  	// Try to add a charm to the environment without authorization.
   433  	s.DischargeUser = ""
   434  	err = s.APIState.Client().AddCharm(curl)
   435  	c.Assert(err, gc.ErrorMatches, `cannot retrieve charm "cs:~restricted/precise/wordpress-3": cannot get archive: cannot get discharge from ".*": third party refused discharge: cannot discharge: discharge denied`)
   436  
   437  	tryAs := func(user string) error {
   438  		client := csclient.New(csclient.Params{
   439  			URL: s.Srv.URL,
   440  		})
   441  		s.DischargeUser = user
   442  		var m *macaroon.Macaroon
   443  		err = client.Get("/delegatable-macaroon", &m)
   444  		c.Assert(err, gc.IsNil)
   445  
   446  		return service.AddCharmWithAuthorization(s.State, params.AddCharmWithAuthorization{URL: curl.String()})
   447  	}
   448  	// Try again with authorization for the wrong user.
   449  	err = tryAs("joe")
   450  	c.Assert(err, gc.ErrorMatches, `cannot retrieve charm "cs:~restricted/precise/wordpress-3": cannot get archive: unauthorized: access denied for user "joe"`)
   451  
   452  	// Try again with the correct authorization this time.
   453  	err = tryAs("bob")
   454  	c.Assert(err, gc.IsNil)
   455  
   456  	// Verify that it has actually been uploaded.
   457  	_, err = s.State.Charm(curl)
   458  	c.Assert(err, gc.IsNil)
   459  }
   460  
   461  func (s *serviceSuite) TestAddCharmConcurrently(c *gc.C) {
   462  	var putBarrier sync.WaitGroup
   463  	var blobs blobs
   464  	s.PatchValue(service.NewStateStorage, func(uuid string, session *mgo.Session) statestorage.Storage {
   465  		storage := statestorage.NewStorage(uuid, session)
   466  		return &recordingStorage{Storage: storage, blobs: &blobs, putBarrier: &putBarrier}
   467  	})
   468  
   469  	client := s.APIState.Client()
   470  	curl, _ := s.UploadCharm(c, "trusty/wordpress-3", "wordpress")
   471  
   472  	// Try adding the same charm concurrently from multiple goroutines
   473  	// to test no "duplicate key errors" are reported (see lp bug
   474  	// #1067979) and also at the end only one charm document is
   475  	// created.
   476  
   477  	var wg sync.WaitGroup
   478  	// We don't add them 1-by-1 because that would allow each goroutine to
   479  	// finish separately without actually synchronizing between them
   480  	putBarrier.Add(10)
   481  	for i := 0; i < 10; i++ {
   482  		wg.Add(1)
   483  		go func(index int) {
   484  			defer wg.Done()
   485  
   486  			c.Assert(client.AddCharm(curl), gc.IsNil, gc.Commentf("goroutine %d", index))
   487  			sch, err := s.State.Charm(curl)
   488  			c.Assert(err, gc.IsNil, gc.Commentf("goroutine %d", index))
   489  			c.Assert(sch.URL(), jc.DeepEquals, curl, gc.Commentf("goroutine %d", index))
   490  		}(i)
   491  	}
   492  	wg.Wait()
   493  
   494  	blobs.Lock()
   495  
   496  	c.Assert(blobs.m, gc.HasLen, 10)
   497  
   498  	// Verify there is only a single uploaded charm remains and it
   499  	// contains the correct data.
   500  	sch, err := s.State.Charm(curl)
   501  	c.Assert(err, jc.ErrorIsNil)
   502  	storagePath := sch.StoragePath()
   503  	c.Assert(blobs.m[storagePath], jc.IsTrue)
   504  	for path, exists := range blobs.m {
   505  		if path != storagePath {
   506  			c.Assert(exists, jc.IsFalse)
   507  		}
   508  	}
   509  
   510  	storage := statestorage.NewStorage(s.State.EnvironUUID(), s.State.MongoSession())
   511  	s.assertUploaded(c, storage, sch.StoragePath(), sch.BundleSha256())
   512  }
   513  
   514  func (s *serviceSuite) assertUploaded(c *gc.C, storage statestorage.Storage, storagePath, expectedSHA256 string) {
   515  	reader, _, err := storage.Get(storagePath)
   516  	c.Assert(err, jc.ErrorIsNil)
   517  	defer reader.Close()
   518  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   519  	c.Assert(err, jc.ErrorIsNil)
   520  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   521  }
   522  
   523  func (s *serviceSuite) TestAddCharmOverwritesPlaceholders(c *gc.C) {
   524  	client := s.APIState.Client()
   525  	curl, _ := s.UploadCharm(c, "trusty/wordpress-42", "wordpress")
   526  
   527  	// Add a placeholder with the same charm URL.
   528  	err := s.State.AddStoreCharmPlaceholder(curl)
   529  	c.Assert(err, jc.ErrorIsNil)
   530  	_, err = s.State.Charm(curl)
   531  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   532  
   533  	// Now try to add the charm, which will convert the placeholder to
   534  	// a pending charm.
   535  	err = client.AddCharm(curl)
   536  	c.Assert(err, jc.ErrorIsNil)
   537  
   538  	// Make sure the document's flags were reset as expected.
   539  	sch, err := s.State.Charm(curl)
   540  	c.Assert(err, jc.ErrorIsNil)
   541  	c.Assert(sch.URL(), jc.DeepEquals, curl)
   542  	c.Assert(sch.IsPlaceholder(), jc.IsFalse)
   543  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   544  }
   545  
   546  type mockStorageProvider struct {
   547  	storage.Provider
   548  	kind storage.StorageKind
   549  }
   550  
   551  func (m *mockStorageProvider) Scope() storage.Scope {
   552  	return storage.ScopeMachine
   553  }
   554  
   555  func (m *mockStorageProvider) Supports(k storage.StorageKind) bool {
   556  	return k == m.kind
   557  }
   558  
   559  func (m *mockStorageProvider) ValidateConfig(*storage.Config) error {
   560  	return nil
   561  }
   562  
   563  type blobs struct {
   564  	sync.Mutex
   565  	m map[string]bool // maps path to added (true), or deleted (false)
   566  }
   567  
   568  // Add adds a path to the list of known paths.
   569  func (b *blobs) Add(path string) {
   570  	b.Lock()
   571  	defer b.Unlock()
   572  	b.check()
   573  	b.m[path] = true
   574  }
   575  
   576  // Remove marks a path as deleted, even if it was not previously Added.
   577  func (b *blobs) Remove(path string) {
   578  	b.Lock()
   579  	defer b.Unlock()
   580  	b.check()
   581  	b.m[path] = false
   582  }
   583  
   584  func (b *blobs) check() {
   585  	if b.m == nil {
   586  		b.m = make(map[string]bool)
   587  	}
   588  }
   589  
   590  type recordingStorage struct {
   591  	statestorage.Storage
   592  	putBarrier *sync.WaitGroup
   593  	blobs      *blobs
   594  }
   595  
   596  func (s *recordingStorage) Put(path string, r io.Reader, size int64) error {
   597  	if s.putBarrier != nil {
   598  		// This goroutine has gotten to Put() so mark it Done() and
   599  		// wait for the other goroutines to get to this point.
   600  		s.putBarrier.Done()
   601  		s.putBarrier.Wait()
   602  	}
   603  	if err := s.Storage.Put(path, r, size); err != nil {
   604  		return errors.Trace(err)
   605  	}
   606  	s.blobs.Add(path)
   607  	return nil
   608  }
   609  
   610  func (s *recordingStorage) Remove(path string) error {
   611  	if err := s.Storage.Remove(path); err != nil {
   612  		return errors.Trace(err)
   613  	}
   614  	s.blobs.Remove(path)
   615  	return nil
   616  }