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  }