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: ¶ms.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 }