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