github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/charm_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state_test 5 6 import ( 7 "bytes" 8 "fmt" 9 "path/filepath" 10 "strings" 11 12 "github.com/juju/charm/v12" 13 "github.com/juju/errors" 14 "github.com/juju/mgo/v3" 15 jc "github.com/juju/testing/checkers" 16 "github.com/juju/utils/v3" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/state" 20 "github.com/juju/juju/state/storage" 21 "github.com/juju/juju/testcharms" 22 "github.com/juju/juju/testing/factory" 23 ) 24 25 // TODO (hml) lxd-profile 26 // Go back and add additional tests here 27 28 type CharmSuite struct { 29 ConnSuite 30 charm *state.Charm 31 curl string 32 } 33 34 var _ = gc.Suite(&CharmSuite{}) 35 36 func (s *CharmSuite) SetUpTest(c *gc.C) { 37 s.ConnSuite.SetUpTest(c) 38 s.charm = s.AddTestingCharm(c, "dummy") 39 s.curl = s.charm.URL() 40 } 41 42 func (s *CharmSuite) destroy(c *gc.C) { 43 err := s.charm.Destroy() 44 c.Assert(err, jc.ErrorIsNil) 45 } 46 47 func (s *CharmSuite) remove(c *gc.C) { 48 s.destroy(c) 49 err := s.charm.Remove() 50 c.Assert(err, jc.ErrorIsNil) 51 } 52 53 func (s *CharmSuite) checkRemoved(c *gc.C) { 54 _, err := s.State.Charm(s.curl) 55 c.Check(err, gc.ErrorMatches, `charm ".*" not found`) 56 c.Check(err, jc.Satisfies, errors.IsNotFound) 57 58 // Ensure the document is actually gone. 59 coll, closer := state.GetCollection(s.State, "charms") 60 defer closer() 61 count, err := coll.FindId(s.curl).Count() 62 c.Assert(err, jc.ErrorIsNil) 63 c.Check(count, gc.Equals, 0) 64 } 65 66 func (s *CharmSuite) TestAliveCharm(c *gc.C) { 67 s.testCharm(c) 68 } 69 70 func (s *CharmSuite) TestDyingCharm(c *gc.C) { 71 s.destroy(c) 72 s.testCharm(c) 73 } 74 75 func (s *CharmSuite) testCharm(c *gc.C) { 76 dummy, err := s.State.Charm(s.curl) 77 c.Assert(err, jc.ErrorIsNil) 78 c.Assert(dummy.URL(), gc.Equals, s.curl) 79 c.Assert(dummy.Revision(), gc.Equals, 1) 80 c.Assert(dummy.StoragePath(), gc.Equals, "dummy-path") 81 c.Assert(dummy.BundleSha256(), gc.Equals, "quantal-dummy-1-sha256") 82 c.Assert(dummy.IsUploaded(), jc.IsTrue) 83 meta := dummy.Meta() 84 c.Assert(meta.Name, gc.Equals, "dummy") 85 config := dummy.Config() 86 c.Assert(config.Options["title"], gc.Equals, 87 charm.Option{ 88 Default: "My Title", 89 Description: "A descriptive title used for the application.", 90 Type: "string", 91 }, 92 ) 93 actions := dummy.Actions() 94 c.Assert(actions, gc.NotNil) 95 c.Assert(actions.ActionSpecs, gc.Not(gc.HasLen), 0) 96 c.Assert(actions.ActionSpecs["snapshot"], gc.NotNil) 97 c.Assert(actions.ActionSpecs["snapshot"].Params, gc.Not(gc.HasLen), 0) 98 c.Assert(actions.ActionSpecs["snapshot"], gc.DeepEquals, 99 charm.ActionSpec{ 100 Description: "Take a snapshot of the database.", 101 Params: map[string]interface{}{ 102 "type": "object", 103 "title": "snapshot", 104 "description": "Take a snapshot of the database.", 105 "properties": map[string]interface{}{ 106 "outfile": map[string]interface{}{ 107 "description": "The file to write out to.", 108 "type": "string", 109 "default": "foo.bz2", 110 }, 111 }, 112 }, 113 }) 114 } 115 116 func (s *CharmSuite) TestCharmFromSha256(c *gc.C) { 117 ch, err := s.State.Charm(s.curl) 118 c.Assert(err, jc.ErrorIsNil) 119 120 dummy, err := s.State.CharmFromSha256(ch.BundleSha256()[0:7]) 121 122 c.Assert(err, jc.ErrorIsNil) 123 c.Assert(dummy.URL(), gc.Equals, s.curl) 124 c.Assert(dummy.Revision(), gc.Equals, 1) 125 c.Assert(dummy.StoragePath(), gc.Equals, "dummy-path") 126 c.Assert(dummy.BundleSha256(), gc.Equals, "quantal-dummy-1-sha256") 127 c.Assert(dummy.IsUploaded(), jc.IsTrue) 128 meta := dummy.Meta() 129 c.Assert(meta.Name, gc.Equals, "dummy") 130 config := dummy.Config() 131 c.Assert(config.Options["title"], gc.Equals, 132 charm.Option{ 133 Default: "My Title", 134 Description: "A descriptive title used for the application.", 135 Type: "string", 136 }, 137 ) 138 actions := dummy.Actions() 139 c.Assert(actions, gc.NotNil) 140 c.Assert(actions.ActionSpecs, gc.Not(gc.HasLen), 0) 141 c.Assert(actions.ActionSpecs["snapshot"], gc.NotNil) 142 c.Assert(actions.ActionSpecs["snapshot"].Params, gc.Not(gc.HasLen), 0) 143 c.Assert(actions.ActionSpecs["snapshot"], gc.DeepEquals, 144 charm.ActionSpec{ 145 Description: "Take a snapshot of the database.", 146 Params: map[string]interface{}{ 147 "type": "object", 148 "title": "snapshot", 149 "description": "Take a snapshot of the database.", 150 "properties": map[string]interface{}{ 151 "outfile": map[string]interface{}{ 152 "description": "The file to write out to.", 153 "type": "string", 154 "default": "foo.bz2", 155 }, 156 }, 157 }, 158 }) 159 } 160 161 func (s *CharmSuite) TestRemovedCharmNotFound(c *gc.C) { 162 s.remove(c) 163 s.checkRemoved(c) 164 } 165 166 func (s *CharmSuite) TestRemovedCharmNotListed(c *gc.C) { 167 s.remove(c) 168 charms, err := s.State.AllCharms() 169 c.Check(err, jc.ErrorIsNil) 170 c.Check(charms, gc.HasLen, 0) 171 } 172 173 func (s *CharmSuite) TestRemoveWithoutDestroy(c *gc.C) { 174 err := s.charm.Remove() 175 c.Assert(err, gc.ErrorMatches, "still alive") 176 } 177 178 func (s *CharmSuite) TestCharmNotFound(c *gc.C) { 179 curl := "local:anotherseries/dummy-1" 180 _, err := s.State.Charm(curl) 181 c.Assert(err, gc.ErrorMatches, `charm "local:anotherseries/dummy-1" not found`) 182 c.Assert(err, jc.Satisfies, errors.IsNotFound) 183 } 184 185 func (s *CharmSuite) TestCharmFromSha256NotFound(c *gc.C) { 186 _, err := s.State.CharmFromSha256("abcd0123") 187 c.Assert(err, gc.ErrorMatches, `charm with sha256 "abcd0123" not found`) 188 c.Assert(err, jc.Satisfies, errors.IsNotFound) 189 } 190 191 func (s *CharmSuite) dummyCharm(c *gc.C, curlOverride string) state.CharmInfo { 192 info := state.CharmInfo{ 193 Charm: testcharms.Repo.CharmDir("dummy"), 194 StoragePath: "dummy-1", 195 SHA256: "dummy-1-sha256", 196 Version: "dummy-146-g725cfd3-dirty", 197 } 198 if curlOverride != "" { 199 info.ID = curlOverride 200 } else { 201 info.ID = fmt.Sprintf("local:quantal/%s-%d", info.Charm.Meta().Name, info.Charm.Revision()) 202 } 203 return info 204 } 205 206 func (s *CharmSuite) TestRemoveDeletesStorage(c *gc.C) { 207 // We normally don't actually set up charm storage in state 208 // tests, but we need it here. 209 path := s.charm.StoragePath() 210 stor := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession()) 211 err := stor.Put(path, strings.NewReader("abc"), 3) 212 c.Assert(err, jc.ErrorIsNil) 213 214 s.destroy(c) 215 closer, _, err := stor.Get(path) 216 c.Assert(err, jc.ErrorIsNil) 217 closer.Close() 218 219 s.remove(c) 220 _, _, err = stor.Get(path) 221 c.Assert(err, jc.Satisfies, errors.IsNotFound) 222 } 223 224 func (s *CharmSuite) TestReferenceDyingCharm(c *gc.C) { 225 226 s.destroy(c) 227 228 args := state.AddApplicationArgs{ 229 Name: "blah", 230 Charm: s.charm, 231 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 232 OS: "ubuntu", 233 Channel: "22.04/stable", 234 }}, 235 } 236 _, err := s.State.AddApplication(args) 237 c.Check(err, gc.ErrorMatches, `cannot add application "blah": charm: not found or not alive`) 238 } 239 240 func (s *CharmSuite) TestReferenceDyingCharmRace(c *gc.C) { 241 242 defer state.SetBeforeHooks(c, s.State, func() { 243 s.destroy(c) 244 }).Check() 245 246 args := state.AddApplicationArgs{ 247 Name: "blah", 248 Charm: s.charm, 249 CharmOrigin: &state.CharmOrigin{Platform: &state.Platform{ 250 OS: "ubuntu", 251 Channel: "22.04/stable", 252 }}, 253 } 254 _, err := s.State.AddApplication(args) 255 c.Check(err, gc.ErrorMatches, `cannot add application "blah": charm: not found or not alive`) 256 } 257 258 func (s *CharmSuite) TestDestroyReferencedCharm(c *gc.C) { 259 s.Factory.MakeApplication(c, &factory.ApplicationParams{ 260 Charm: s.charm, 261 }) 262 263 err := s.charm.Destroy() 264 c.Check(err, gc.ErrorMatches, "charm in use") 265 } 266 267 func (s *CharmSuite) TestDestroyReferencedCharmRace(c *gc.C) { 268 269 defer state.SetBeforeHooks(c, s.State, func() { 270 s.Factory.MakeApplication(c, &factory.ApplicationParams{ 271 Charm: s.charm, 272 }) 273 }).Check() 274 275 err := s.charm.Destroy() 276 c.Check(err, gc.ErrorMatches, "charm in use") 277 } 278 279 func (s *CharmSuite) TestDestroyUnreferencedCharm(c *gc.C) { 280 app := s.Factory.MakeApplication(c, &factory.ApplicationParams{ 281 Charm: s.charm, 282 }) 283 err := app.Destroy() 284 c.Assert(err, jc.ErrorIsNil) 285 286 err = s.charm.Destroy() 287 c.Assert(err, jc.ErrorIsNil) 288 } 289 290 func (s *CharmSuite) TestDestroyUnitReferencedCharm(c *gc.C) { 291 app := s.Factory.MakeApplication(c, &factory.ApplicationParams{ 292 Charm: s.charm, 293 }) 294 unit := s.Factory.MakeUnit(c, &factory.UnitParams{ 295 Application: app, 296 SetCharmURL: true, 297 }) 298 299 // set app charm to something different 300 info := s.dummyCharm(c, "ch:quantal/dummy-2") 301 newCh, err := s.State.AddCharm(info) 302 c.Assert(err, jc.ErrorIsNil) 303 err = app.SetCharm(state.SetCharmConfig{Charm: newCh, CharmOrigin: defaultCharmOrigin(newCh.URL())}) 304 c.Assert(err, jc.ErrorIsNil) 305 306 // unit should still reference original charm until updated 307 err = s.charm.Destroy() 308 c.Assert(err, gc.ErrorMatches, "charm in use") 309 err = unit.SetCharmURL(info.ID) 310 c.Assert(err, jc.ErrorIsNil) 311 err = s.charm.Destroy() 312 c.Assert(err, jc.ErrorIsNil) 313 } 314 315 func (s *CharmSuite) TestDestroyFinalUnitReference(c *gc.C) { 316 app := s.Factory.MakeApplication(c, &factory.ApplicationParams{ 317 Charm: s.charm, 318 }) 319 unit, err := app.AddUnit(state.AddUnitParams{}) 320 c.Assert(err, jc.ErrorIsNil) 321 322 c.Logf("calling app.Destroy()") 323 c.Assert(app.Destroy(), jc.ErrorIsNil) 324 removeUnit(c, unit) 325 326 assertCleanupCount(c, s.State, 2) 327 s.checkRemoved(c) 328 } 329 330 func (s *CharmSuite) TestAddCharm(c *gc.C) { 331 // Check that adding charms from scratch works correctly. 332 info := s.dummyCharm(c, "") 333 dummy, err := s.State.AddCharm(info) 334 c.Assert(err, jc.ErrorIsNil) 335 c.Assert(dummy.URL(), gc.Equals, info.ID) 336 337 doc := state.CharmDoc{} 338 err = s.charms.FindId(state.DocID(s.State, info.ID)).One(&doc) 339 c.Assert(err, jc.ErrorIsNil) 340 c.Logf("%#v", doc) 341 c.Assert(*doc.URL, gc.DeepEquals, info.ID) 342 343 expVersion := "dummy-146-g725cfd3-dirty" 344 c.Assert(doc.CharmVersion, gc.Equals, expVersion) 345 } 346 347 func (s *CharmSuite) TestAddCharmUpdatesPlaceholder(c *gc.C) { 348 // Check that adding charms updates any existing placeholder charm 349 // with the same URL. 350 ch := testcharms.Repo.CharmDir("dummy") 351 352 // Add a placeholder charm. 353 curl := charm.MustParseURL("ch:quantal/dummy-1") 354 err := s.State.AddCharmPlaceholder(curl) 355 c.Assert(err, jc.ErrorIsNil) 356 357 // Add a deployed charm. 358 info := state.CharmInfo{ 359 Charm: ch, 360 ID: curl.String(), 361 StoragePath: "dummy-1", 362 SHA256: "dummy-1-sha256", 363 } 364 dummy, err := s.State.AddCharm(info) 365 c.Assert(err, jc.ErrorIsNil) 366 c.Assert(dummy.URL(), gc.Equals, curl.String()) 367 368 // Charm doc has been updated. 369 var docs []state.CharmDoc 370 err = s.charms.FindId(state.DocID(s.State, curl.String())).All(&docs) 371 c.Assert(err, jc.ErrorIsNil) 372 c.Assert(docs, gc.HasLen, 1) 373 c.Assert(*docs[0].URL, gc.DeepEquals, curl.String()) 374 c.Assert(docs[0].StoragePath, gc.DeepEquals, info.StoragePath) 375 376 // No more placeholder charm. 377 _, err = s.State.LatestPlaceholderCharm(curl) 378 c.Assert(err, jc.Satisfies, errors.IsNotFound) 379 } 380 381 func (s *CharmSuite) assertPendingCharmExists(c *gc.C, curl string) { 382 // Find charm directly and verify only the charm URL and 383 // PendingUpload are set. 384 doc := state.CharmDoc{} 385 err := s.charms.FindId(state.DocID(s.State, curl)).One(&doc) 386 c.Assert(err, jc.ErrorIsNil) 387 c.Logf("%#v", doc) 388 c.Assert(*doc.URL, gc.DeepEquals, curl) 389 c.Assert(doc.PendingUpload, jc.IsTrue) 390 c.Assert(doc.Placeholder, jc.IsFalse) 391 c.Assert(doc.Meta, gc.IsNil) 392 c.Assert(doc.Config, gc.IsNil) 393 c.Assert(doc.StoragePath, gc.Equals, "") 394 c.Assert(doc.BundleSha256, gc.Equals, "") 395 } 396 397 func (s *CharmSuite) TestAddCharmWithInvalidMetaData(c *gc.C) { 398 check := func(munge func(meta *charm.Meta)) { 399 info := s.dummyCharm(c, "") 400 meta := info.Charm.Meta() 401 munge(meta) 402 _, err := s.State.AddCharm(info) 403 c.Assert(err, gc.ErrorMatches, `invalid charm data: "\$foo" is not a valid field name`) 404 } 405 406 check(func(meta *charm.Meta) { 407 meta.Provides = map[string]charm.Relation{"$foo": {}} 408 }) 409 check(func(meta *charm.Meta) { 410 meta.Requires = map[string]charm.Relation{"$foo": {}} 411 }) 412 check(func(meta *charm.Meta) { 413 meta.Peers = map[string]charm.Relation{"$foo": {}} 414 }) 415 } 416 417 func (s *CharmSuite) TestPrepareLocalCharmUpload(c *gc.C) { 418 // First test the sanity checks. 419 curl, err := s.State.PrepareLocalCharmUpload("local:quantal/dummy") 420 c.Assert(err, gc.ErrorMatches, "expected charm URL with revision, got .*") 421 c.Assert(curl, gc.IsNil) 422 curl, err = s.State.PrepareLocalCharmUpload("ch:quantal/dummy") 423 c.Assert(err, gc.ErrorMatches, "expected charm URL with local schema, got .*") 424 c.Assert(curl, gc.IsNil) 425 426 // No charm in state, so the call should respect given revision. 427 testCurl := "local:quantal/missing-123" 428 curl, err = s.State.PrepareLocalCharmUpload(testCurl) 429 c.Assert(err, jc.ErrorIsNil) 430 c.Assert(curl.String(), gc.Equals, testCurl) 431 s.assertPendingCharmExists(c, curl.String()) 432 433 // Make sure we can't find it with st.Charm(). 434 _, err = s.State.Charm(curl.String()) 435 c.Assert(err, jc.Satisfies, errors.IsNotFound) 436 437 // Try adding it again with the same revision and ensure it gets bumped. 438 curl, err = s.State.PrepareLocalCharmUpload(curl.String()) 439 c.Assert(err, jc.ErrorIsNil) 440 c.Assert(curl.Revision, gc.Equals, 124) 441 442 // Also ensure the revision cannot decrease. 443 curl, err = s.State.PrepareLocalCharmUpload(curl.WithRevision(42).String()) 444 c.Assert(err, jc.ErrorIsNil) 445 c.Assert(curl.Revision, gc.Equals, 125) 446 447 // Check the given revision is respected. 448 curl, err = s.State.PrepareLocalCharmUpload(curl.WithRevision(1234).String()) 449 c.Assert(err, jc.ErrorIsNil) 450 c.Assert(curl.Revision, gc.Equals, 1234) 451 } 452 453 func (s *CharmSuite) TestPrepareLocalCharmUploadRemoved(c *gc.C) { 454 // Remove the fixture charm and try to re-add it; it gets a new 455 // revision. 456 s.remove(c) 457 curl, err := s.State.PrepareLocalCharmUpload(s.curl) 458 c.Assert(err, jc.ErrorIsNil) 459 c.Assert(curl.Revision, gc.Equals, charm.MustParseURL(s.curl).Revision+1) 460 } 461 462 func (s *CharmSuite) TestPrepareCharmUpload(c *gc.C) { 463 // First test the sanity checks. 464 sch, err := s.State.PrepareCharmUpload("ch:quantal/dummy") 465 c.Assert(err, gc.ErrorMatches, "expected charm URL with revision, got .*") 466 c.Assert(sch, gc.IsNil) 467 sch, err = s.State.PrepareCharmUpload("local:quantal/dummy") 468 c.Assert(err, gc.ErrorMatches, "expected charm URL with a valid schema, got .*") 469 c.Assert(sch, gc.IsNil) 470 471 // No charm in state, so the call should respect given revision. 472 testCurl := "ch:quantal/missing-123" 473 sch, err = s.State.PrepareCharmUpload(testCurl) 474 c.Assert(err, jc.ErrorIsNil) 475 c.Assert(sch.URL(), gc.DeepEquals, testCurl) 476 c.Assert(sch.IsUploaded(), jc.IsFalse) 477 478 s.assertPendingCharmExists(c, sch.URL()) 479 // Make sure we can find it with st.Charm(). 480 found, err := s.State.Charm(sch.URL()) 481 c.Assert(err, jc.ErrorIsNil) 482 c.Assert(found.URL(), gc.Equals, sch.URL()) 483 484 // Try adding it again with the same revision and ensure we get the same document. 485 schCopy, err := s.State.PrepareCharmUpload(testCurl) 486 c.Assert(err, jc.ErrorIsNil) 487 // URL is required to set the charmURL, so the test will succeed. 488 _ = schCopy.URL() 489 c.Assert(sch, jc.DeepEquals, schCopy) 490 491 // Now add a charm and try again - we should get the same result 492 // as with AddCharm. 493 info := s.dummyCharm(c, "ch:precise/dummy-2") 494 sch, err = s.State.AddCharm(info) 495 c.Assert(err, jc.ErrorIsNil) 496 schCopy, err = s.State.PrepareCharmUpload(info.ID) 497 c.Assert(err, jc.ErrorIsNil) 498 c.Assert(sch, jc.DeepEquals, schCopy) 499 } 500 501 func (s *CharmSuite) TestUpdateUploadedCharm(c *gc.C) { 502 info := s.dummyCharm(c, "") 503 _, err := s.State.AddCharm(info) 504 c.Assert(err, jc.ErrorIsNil) 505 506 // Test with already uploaded and a missing charms. 507 sch, err := s.State.UpdateUploadedCharm(info) 508 c.Assert(err, gc.ErrorMatches, fmt.Sprintf("charm %q already uploaded", info.ID)) 509 c.Assert(sch, gc.IsNil) 510 info.ID = "local:quantal/missing-1" 511 info.SHA256 = "missing" 512 sch, err = s.State.UpdateUploadedCharm(info) 513 c.Assert(err, jc.Satisfies, errors.IsNotFound) 514 c.Assert(sch, gc.IsNil) 515 516 // Test with an uploaded local charm. 517 _, err = s.State.PrepareLocalCharmUpload(info.ID) 518 c.Assert(err, jc.ErrorIsNil) 519 520 sch, err = s.State.UpdateUploadedCharm(info) 521 c.Assert(err, jc.ErrorIsNil) 522 c.Assert(sch.URL(), gc.DeepEquals, info.ID) 523 c.Assert(sch.Revision(), gc.Equals, charm.MustParseURL(info.ID).Revision) 524 c.Assert(sch.IsUploaded(), jc.IsTrue) 525 c.Assert(sch.IsPlaceholder(), jc.IsFalse) 526 c.Assert(sch.Meta(), gc.DeepEquals, info.Charm.Meta()) 527 c.Assert(sch.Config(), gc.DeepEquals, info.Charm.Config()) 528 c.Assert(sch.StoragePath(), gc.DeepEquals, info.StoragePath) 529 c.Assert(sch.BundleSha256(), gc.Equals, "missing") 530 } 531 532 func (s *CharmSuite) TestUpdateUploadedCharmEscapesSpecialCharsInConfig(c *gc.C) { 533 // Make sure when we have mongodb special characters like "$" and 534 // "." in the name of any charm config option, we do proper 535 // escaping before storing them and unescaping after loading. See 536 // also http://pad.lv/1308146. 537 538 // Clone the dummy charm and change the config. 539 configWithProblematicKeys := []byte(` 540 options: 541 $bad.key: {default: bad, description: bad, type: string} 542 not.ok.key: {description: not ok, type: int} 543 valid-key: {description: all good, type: boolean} 544 still$bad.: {description: not good, type: float} 545 $.$: {description: awful, type: string} 546 ...: {description: oh boy, type: int} 547 just$: {description: no no, type: float} 548 `[1:]) 549 chDir := testcharms.Repo.ClonedDirPath(c.MkDir(), "dummy") 550 err := utils.AtomicWriteFile( 551 filepath.Join(chDir, "config.yaml"), 552 configWithProblematicKeys, 553 0666, 554 ) 555 c.Assert(err, jc.ErrorIsNil) 556 ch, err := charm.ReadCharmDir(chDir) 557 c.Assert(err, jc.ErrorIsNil) 558 missingCurl := "local:quantal/missing-1" 559 storagePath := "dummy-1" 560 561 preparedCurl, err := s.State.PrepareLocalCharmUpload(missingCurl) 562 c.Assert(err, jc.ErrorIsNil) 563 info := state.CharmInfo{ 564 Charm: ch, 565 ID: preparedCurl.String(), 566 StoragePath: "dummy-1", 567 SHA256: "missing", 568 } 569 sch, err := s.State.UpdateUploadedCharm(info) 570 c.Assert(err, jc.ErrorIsNil) 571 c.Assert(sch.URL(), gc.DeepEquals, missingCurl) 572 c.Assert(sch.Revision(), gc.Equals, charm.MustParseURL(missingCurl).Revision) 573 c.Assert(sch.IsUploaded(), jc.IsTrue) 574 c.Assert(sch.IsPlaceholder(), jc.IsFalse) 575 c.Assert(sch.Meta(), gc.DeepEquals, ch.Meta()) 576 c.Assert(sch.Config(), gc.DeepEquals, ch.Config()) 577 c.Assert(sch.StoragePath(), gc.DeepEquals, storagePath) 578 c.Assert(sch.BundleSha256(), gc.Equals, "missing") 579 } 580 581 func (s *CharmSuite) assertPlaceholderCharmExists(c *gc.C, curl string) { 582 // Find charm directly and verify only the charm URL and 583 // Placeholder are set. 584 doc := state.CharmDoc{} 585 err := s.charms.FindId(state.DocID(s.State, curl)).One(&doc) 586 c.Assert(err, jc.ErrorIsNil) 587 c.Assert(*doc.URL, gc.DeepEquals, curl) 588 c.Assert(doc.PendingUpload, jc.IsFalse) 589 c.Assert(doc.Placeholder, jc.IsTrue) 590 c.Assert(doc.Meta, gc.IsNil) 591 c.Assert(doc.Config, gc.IsNil) 592 c.Assert(doc.StoragePath, gc.Equals, "") 593 c.Assert(doc.BundleSha256, gc.Equals, "") 594 595 // Make sure we can't find it with st.Charm(). 596 _, err = s.State.Charm(curl) 597 c.Assert(err, jc.Satisfies, errors.IsNotFound) 598 } 599 600 func (s *CharmSuite) TestUpdateUploadedCharmRejectsInvalidMetadata(c *gc.C) { 601 info := s.dummyCharm(c, "") 602 _, err := s.State.PrepareLocalCharmUpload(info.ID) 603 c.Assert(err, jc.ErrorIsNil) 604 605 meta := info.Charm.Meta() 606 meta.Provides = map[string]charm.Relation{ 607 "foo.bar": {}, 608 } 609 _, err = s.State.UpdateUploadedCharm(info) 610 c.Assert(err, gc.ErrorMatches, `invalid charm data: "foo.bar" is not a valid field name`) 611 } 612 613 func (s *CharmSuite) TestLatestPlaceholderCharm(c *gc.C) { 614 // Add a deployed charm 615 info := s.dummyCharm(c, "ch:quantal/dummy-1") 616 _, err := s.State.AddCharm(info) 617 c.Assert(err, jc.ErrorIsNil) 618 619 // Deployed charm not found. 620 _, err = s.State.LatestPlaceholderCharm(charm.MustParseURL(info.ID)) 621 c.Assert(err, jc.Satisfies, errors.IsNotFound) 622 623 // Add a charm reference 624 curl2 := charm.MustParseURL("ch:quantal/dummy-2") 625 err = s.State.AddCharmPlaceholder(curl2) 626 c.Assert(err, jc.ErrorIsNil) 627 s.assertPlaceholderCharmExists(c, curl2.String()) 628 629 // Use a URL with an arbitrary rev to search. 630 curl := charm.MustParseURL("ch:quantal/dummy-23") 631 pending, err := s.State.LatestPlaceholderCharm(curl) 632 c.Assert(err, jc.ErrorIsNil) 633 c.Assert(pending.URL(), gc.Equals, curl2.String()) 634 c.Assert(pending.IsPlaceholder(), jc.IsTrue) 635 c.Assert(pending.Meta(), gc.IsNil) 636 c.Assert(pending.Config(), gc.IsNil) 637 c.Assert(pending.StoragePath(), gc.Equals, "") 638 c.Assert(pending.BundleSha256(), gc.Equals, "") 639 } 640 641 func (s *CharmSuite) TestAddCharmPlaceholderErrors(c *gc.C) { 642 ch := testcharms.Repo.CharmDir("dummy") 643 curl := charm.MustParseURL( 644 fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()), 645 ) 646 err := s.State.AddCharmPlaceholder(curl) 647 c.Assert(err, gc.ErrorMatches, "expected charm URL with a valid schema, got .*") 648 649 curl = charm.MustParseURL("ch:quantal/dummy") 650 err = s.State.AddCharmPlaceholder(curl) 651 c.Assert(err, gc.ErrorMatches, "expected charm URL with revision, got .*") 652 } 653 654 func (s *CharmSuite) TestAddCharmPlaceholder(c *gc.C) { 655 curl := charm.MustParseURL("ch:quantal/dummy-1") 656 err := s.State.AddCharmPlaceholder(curl) 657 c.Assert(err, jc.ErrorIsNil) 658 s.assertPlaceholderCharmExists(c, curl.String()) 659 660 // Add the same one again, should be a no-op 661 err = s.State.AddCharmPlaceholder(curl) 662 c.Assert(err, jc.ErrorIsNil) 663 s.assertPlaceholderCharmExists(c, curl.String()) 664 } 665 666 func (s *CharmSuite) assertAddCharmPlaceholder(c *gc.C) (string, *charm.URL, *state.Charm) { 667 // Add a deployed charm 668 info := s.dummyCharm(c, "ch:quantal/dummy-1") 669 dummy, err := s.State.AddCharm(info) 670 c.Assert(err, jc.ErrorIsNil) 671 672 // Add a charm placeholder 673 curl2 := charm.MustParseURL("ch:quantal/dummy-2") 674 err = s.State.AddCharmPlaceholder(curl2) 675 c.Assert(err, jc.ErrorIsNil) 676 s.assertPlaceholderCharmExists(c, curl2.String()) 677 678 // Deployed charm is still there. 679 existing, err := s.State.Charm(info.ID) 680 c.Assert(err, jc.ErrorIsNil) 681 c.Assert(existing, jc.DeepEquals, dummy) 682 683 return info.ID, curl2, dummy 684 } 685 686 func (s *CharmSuite) TestAddCharmPlaceholderLeavesDeployedCharmsAlone(c *gc.C) { 687 s.assertAddCharmPlaceholder(c) 688 } 689 690 func (s *CharmSuite) TestAddCharmPlaceholderDeletesOlder(c *gc.C) { 691 curl, curlOldRef, dummy := s.assertAddCharmPlaceholder(c) 692 693 // Add a new charm placeholder 694 curl3 := charm.MustParseURL("ch:quantal/dummy-3") 695 err := s.State.AddCharmPlaceholder(curl3) 696 c.Assert(err, jc.ErrorIsNil) 697 s.assertPlaceholderCharmExists(c, curl3.String()) 698 699 // Deployed charm is still there. 700 existing, err := s.State.Charm(curl) 701 c.Assert(err, jc.ErrorIsNil) 702 c.Assert(existing, jc.DeepEquals, dummy) 703 704 // Older charm placeholder is gone. 705 doc := state.CharmDoc{} 706 err = s.charms.FindId(curlOldRef).One(&doc) 707 c.Assert(err, gc.Equals, mgo.ErrNotFound) 708 } 709 710 func (s *CharmSuite) TestAllCharms(c *gc.C) { 711 // Add a deployed charm 712 info := s.dummyCharm(c, "ch:quantal/dummy-1") 713 sch, err := s.State.AddCharm(info) 714 c.Assert(err, jc.ErrorIsNil) 715 716 // Add a charm reference 717 curl2 := charm.MustParseURL("ch:quantal/dummy-2") 718 err = s.State.AddCharmPlaceholder(curl2) 719 c.Assert(err, jc.ErrorIsNil) 720 721 charms, err := s.State.AllCharms() 722 c.Assert(err, jc.ErrorIsNil) 723 c.Assert(charms, gc.HasLen, 3) 724 725 c.Assert(charms[0].URL(), gc.Equals, "local:quantal/quantal-dummy-1") 726 c.Assert(charms[1], gc.DeepEquals, sch) 727 c.Assert(charms[2].URL(), gc.Equals, curl2.String()) 728 } 729 730 func (s *CharmSuite) TestAddCharmMetadata(c *gc.C) { 731 // Check that a charm with missing sha/storage path is flagged as pending 732 // to be uploaded. 733 dummy1 := s.dummyCharm(c, "ch:quantal/dummy-1") 734 dummy1.SHA256 = "" 735 dummy1.StoragePath = "" 736 ch1, err := s.State.AddCharmMetadata(dummy1) 737 c.Assert(err, jc.ErrorIsNil) 738 c.Check(ch1.IsPlaceholder(), jc.IsFalse) 739 c.Check(ch1.IsUploaded(), jc.IsFalse, gc.Commentf("expected charm with missing SHA/storage path to have the PendingUpload flag set")) 740 741 // Check that uploading the same charm ID yields the same charm 742 ch, err := s.State.AddCharmMetadata(dummy1) 743 c.Assert(err, jc.ErrorIsNil) 744 c.Check(ch1, gc.DeepEquals, ch) 745 746 // Check that a charm with populated sha/storage path is flagged as 747 // uploaded. 748 dummy2 := s.dummyCharm(c, "ch:quantal/dummy-2") 749 ch2, err := s.State.AddCharmMetadata(dummy2) 750 c.Assert(err, jc.ErrorIsNil) 751 c.Check(ch2.IsPlaceholder(), jc.IsFalse) 752 c.Check(ch2.IsUploaded(), jc.IsTrue, gc.Commentf("expected charm with populated SHA/storage path to have the PendingUpload flag unset")) 753 } 754 755 func (s *CharmSuite) TestAddCharmMetadataUpdatesPlaceholder(c *gc.C) { 756 // The charm revision updater adds a placeholder charm doc into the db. 757 // Ensure that AddCharmMetadata can handle that. 758 err := s.State.AddCharmPlaceholder(charm.MustParseURL("ch:quantal/testme-2")) 759 c.Assert(err, jc.ErrorIsNil) 760 761 testme := s.dummyCharm(c, "ch:quantal/testme-2") 762 ch2, err := s.State.AddCharmMetadata(testme) 763 c.Assert(err, jc.ErrorIsNil) 764 c.Check(ch2.IsPlaceholder(), jc.IsFalse) 765 } 766 767 func (s *CharmSuite) TestAllCharmURLs(c *gc.C) { 768 ch2 := state.AddTestingCharmhubCharmForSeries(c, s.State, "jammy", "dummy") 769 state.AddTestingApplication(c, s.State, "testme-jammy", ch2) 770 771 curls, err := s.State.AllCharmURLs() 772 c.Assert(err, jc.ErrorIsNil) 773 // One application from SetUpTest 774 c.Assert(len(curls), gc.Equals, 2, gc.Commentf("%v", curls)) 775 } 776 777 type CharmTestHelperSuite struct { 778 ConnSuite 779 } 780 781 var _ = gc.Suite(&CharmTestHelperSuite{}) 782 783 func assertCustomCharm( 784 c *gc.C, 785 ch *state.Charm, 786 series string, 787 meta *charm.Meta, 788 config *charm.Config, 789 metrics *charm.Metrics, 790 revision int, 791 ) { 792 // Check Charm interface method results. 793 c.Assert(ch.Meta(), gc.DeepEquals, meta) 794 c.Assert(ch.Config(), gc.DeepEquals, config) 795 c.Assert(ch.Metrics(), gc.DeepEquals, metrics) 796 c.Assert(ch.Revision(), gc.DeepEquals, revision) 797 798 // Test URL matches charm and expected series. 799 url := charm.MustParseURL(ch.URL()) 800 c.Assert(url.Series, gc.Equals, series) 801 c.Assert(url.Revision, gc.Equals, ch.Revision()) 802 803 // Ignore the StoragePath and BundleSHA256 methods, they're irrelevant. 804 } 805 806 func forEachStandardCharm(c *gc.C, f func(name string)) { 807 for _, name := range []string{ 808 "logging", "mysql", "riak", "wordpress", 809 } { 810 c.Logf("checking %s", name) 811 f(name) 812 } 813 } 814 815 func (s *CharmTestHelperSuite) TestSimple(c *gc.C) { 816 forEachStandardCharm(c, func(name string) { 817 chd := testcharms.Repo.CharmDir(name) 818 meta := chd.Meta() 819 config := chd.Config() 820 metrics := chd.Metrics() 821 revision := chd.Revision() 822 823 ch := s.AddTestingCharm(c, name) 824 assertCustomCharm(c, ch, "quantal", meta, config, metrics, revision) 825 826 ch = s.AddSeriesCharm(c, name, "bionic") 827 assertCustomCharm(c, ch, "bionic", meta, config, metrics, revision) 828 }) 829 } 830 831 var configYaml = ` 832 options: 833 working: 834 description: when set to false, prevents application from functioning correctly 835 default: true 836 type: boolean 837 ` 838 839 func (s *CharmTestHelperSuite) TestConfigCharm(c *gc.C) { 840 config, err := charm.ReadConfig(bytes.NewBuffer([]byte(configYaml))) 841 c.Assert(err, jc.ErrorIsNil) 842 843 forEachStandardCharm(c, func(name string) { 844 chd := testcharms.Repo.CharmDir(name) 845 meta := chd.Meta() 846 metrics := chd.Metrics() 847 ch := s.AddConfigCharm(c, name, configYaml, 123) 848 assertCustomCharm(c, ch, "quantal", meta, config, metrics, 123) 849 }) 850 } 851 852 var actionsYaml = ` 853 actions: 854 dump: 855 description: Dump the database to STDOUT. 856 params: 857 redirect-file: 858 description: Redirect to a log file. 859 type: string 860 ` 861 862 func (s *CharmTestHelperSuite) TestActionsCharm(c *gc.C) { 863 forEachStandardCharm(c, func(name string) { 864 actions, err := charm.ReadActionsYaml(name, bytes.NewBuffer([]byte(actionsYaml))) 865 c.Assert(err, jc.ErrorIsNil) 866 ch := s.AddActionsCharm(c, name, actionsYaml, 123) 867 c.Assert(ch.Actions(), gc.DeepEquals, actions) 868 }) 869 } 870 871 var metricsYaml = ` 872 metrics: 873 blips: 874 description: A custom metric. 875 type: gauge 876 ` 877 878 func (s *CharmTestHelperSuite) TestMetricsCharm(c *gc.C) { 879 metrics, err := charm.ReadMetrics(bytes.NewBuffer([]byte(metricsYaml))) 880 c.Assert(err, jc.ErrorIsNil) 881 882 forEachStandardCharm(c, func(name string) { 883 chd := testcharms.Repo.CharmDir(name) 884 meta := chd.Meta() 885 config := chd.Config() 886 887 ch := s.AddMetricsCharm(c, name, metricsYaml, 123) 888 assertCustomCharm(c, ch, "quantal", meta, config, metrics, 123) 889 }) 890 } 891 892 var metaYamlSnippet = ` 893 summary: blah 894 description: blah blah 895 ` 896 897 func (s *CharmTestHelperSuite) TestMetaCharm(c *gc.C) { 898 forEachStandardCharm(c, func(name string) { 899 chd := testcharms.Repo.CharmDir(name) 900 config := chd.Config() 901 metrics := chd.Metrics() 902 metaYaml := "name: " + name + metaYamlSnippet 903 meta, err := charm.ReadMeta(bytes.NewBuffer([]byte(metaYaml))) 904 c.Assert(err, jc.ErrorIsNil) 905 906 ch := s.AddMetaCharm(c, name, metaYaml, 123) 907 assertCustomCharm(c, ch, "quantal", meta, config, metrics, 123) 908 }) 909 } 910 911 func (s *CharmTestHelperSuite) TestLXDProfileCharm(c *gc.C) { 912 chd := testcharms.Repo.CharmDir("lxd-profile") 913 c.Assert(chd.LXDProfile(), jc.DeepEquals, &charm.LXDProfile{ 914 Config: map[string]string{ 915 "security.nesting": "true", 916 "security.privileged": "true", 917 "linux.kernel_modules": "openvswitch,nbd,ip_tables,ip6_tables", 918 "environment.http_proxy": "", 919 }, 920 Description: "lxd profile for testing, will pass validation", 921 Devices: map[string]map[string]string{ 922 "tun": { 923 "path": "/dev/net/tun", 924 "type": "unix-char", 925 }, 926 "sony": { 927 "type": "usb", 928 "vendorid": "0fce", 929 "productid": "51da", 930 }, 931 "bdisk": { 932 "source": "/dev/loop0", 933 "type": "unix-block", 934 }, 935 "gpu": { 936 "type": "gpu", 937 }, 938 }, 939 }) 940 } 941 942 var manifestYaml = ` 943 bases: 944 - name: ubuntu 945 channel: "18.04" 946 - name: ubuntu 947 channel: "20.04" 948 ` 949 950 func (s *CharmTestHelperSuite) TestManifestCharm(c *gc.C) { 951 manifest, err := charm.ReadManifest(bytes.NewBuffer([]byte(manifestYaml))) 952 c.Assert(err, jc.ErrorIsNil) 953 954 forEachStandardCharm(c, func(name string) { 955 ch := s.AddManifestCharm(c, name, manifestYaml, 123) 956 c.Assert(ch.Manifest(), gc.DeepEquals, manifest) 957 }) 958 } 959 960 func (s *CharmTestHelperSuite) TestTestingCharm(c *gc.C) { 961 added := state.AddTestingCharmFromRepo(c, s.State, "metered", testcharms.CharmRepo()) 962 c.Assert(added.Metrics(), gc.NotNil) 963 964 charmDir := testcharms.CharmRepo().CharmDir("metered") 965 c.Assert(charmDir.Metrics(), gc.DeepEquals, added.Metrics()) 966 }