Пример #1
0
func (s *DirSuite) TestDirRevisionFile(c *C) {
	charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
	revPath := filepath.Join(charmDir, "revision")

	// Missing revision file
	err := os.Remove(revPath)
	c.Assert(err, IsNil)

	dir, err := charm.ReadDir(charmDir)
	c.Assert(err, IsNil)
	c.Assert(dir.Revision(), Equals, 0)

	// Missing revision file with old revision in metadata
	file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
	c.Assert(err, IsNil)
	_, err = file.Write([]byte("\nrevision: 1234\n"))
	c.Assert(err, IsNil)

	dir, err = charm.ReadDir(charmDir)
	c.Assert(err, IsNil)
	c.Assert(dir.Revision(), Equals, 1234)

	// Revision file with bad content
	err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
	c.Assert(err, IsNil)

	dir, err = charm.ReadDir(charmDir)
	c.Assert(err, ErrorMatches, "invalid revision file")
	c.Assert(dir, IsNil)
}
Пример #2
0
func (s *DirSuite) TestDirSetDiskRevision(c *C) {
	charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
	dir, err := charm.ReadDir(charmDir)
	c.Assert(err, IsNil)

	c.Assert(dir.Revision(), Equals, 1)
	dir.SetDiskRevision(42)
	c.Assert(dir.Revision(), Equals, 42)

	dir, err = charm.ReadDir(charmDir)
	c.Assert(err, IsNil)
	c.Assert(dir.Revision(), Equals, 42)
}
Пример #3
0
func (s *DirSuite) TestReadDirWithoutConfig(c *C) {
	path := testing.Charms.DirPath("varnish")
	dir, err := charm.ReadDir(path)
	c.Assert(err, IsNil)

	// A lacking config.yaml file still causes a proper
	// Config value to be returned.
	c.Assert(dir.Config().Options, HasLen, 0)
}
Пример #4
0
func (s *UpgradeCharmSuccessSuite) TestDoesntBumpRevisionWhenNotNecessary(c *C) {
	dir, err := charm.ReadDir(s.path)
	c.Assert(err, IsNil)
	err = dir.SetDiskRevision(42)
	c.Assert(err, IsNil)

	err = runUpgradeCharm(c, "riak")
	c.Assert(err, IsNil)
	s.assertUpgraded(c, 42, false)
	s.assertLocalRevision(c, 42, s.path)
}
Пример #5
0
func (s *DeploySuite) TestUpgradeCharmDir(c *C) {
	dirPath := coretesting.Charms.ClonedDirPath(s.SeriesPath, "dummy")
	err := runDeploy(c, "local:dummy", "-u")
	c.Assert(err, IsNil)
	curl := charm.MustParseURL("local:precise/dummy-2")
	s.AssertService(c, "dummy", curl, 1, 0)
	// Check the charm really was upgraded.
	ch, err := charm.ReadDir(dirPath)
	c.Assert(err, IsNil)
	c.Assert(ch.Revision(), Equals, 2)
}
Пример #6
0
func (s *BundleSuite) TestBundleFileModes(c *C) {
	// Apply subtler mode differences than can be expressed in Bazaar.
	srcPath := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
	modes := []struct {
		path string
		mode os.FileMode
	}{
		{"hooks/install", 0751},
		{"empty", 0750},
		{"src/hello.c", 0614},
	}
	for _, m := range modes {
		err := os.Chmod(filepath.Join(srcPath, m.path), m.mode)
		c.Assert(err, IsNil)
	}
	var haveSymlinks = true
	if err := os.Symlink("../target", filepath.Join(srcPath, "hooks/symlink")); err != nil {
		haveSymlinks = false
	}

	// Bundle and extract the charm to a new directory.
	dir, err := charm.ReadDir(srcPath)
	c.Assert(err, IsNil)
	buf := new(bytes.Buffer)
	err = dir.BundleTo(buf)
	c.Assert(err, IsNil)
	bundle, err := charm.ReadBundleBytes(buf.Bytes())
	c.Assert(err, IsNil)
	path := c.MkDir()
	err = bundle.ExpandTo(path)
	c.Assert(err, IsNil)

	// Check sensible file modes once round-tripped.
	info, err := os.Stat(filepath.Join(path, "src", "hello.c"))
	c.Assert(err, IsNil)
	c.Assert(info.Mode()&0777, Equals, os.FileMode(0644))
	c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))

	info, err = os.Stat(filepath.Join(path, "hooks", "install"))
	c.Assert(err, IsNil)
	c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))
	c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))

	info, err = os.Stat(filepath.Join(path, "empty"))
	c.Assert(err, IsNil)
	c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))

	if haveSymlinks {
		target, err := os.Readlink(filepath.Join(path, "hooks", "symlink"))
		c.Assert(err, IsNil)
		c.Assert(target, Equals, "../target")
	}
}
Пример #7
0
func (s *BundleSuite) TestExpandTo(c *C) {
	bundle, err := charm.ReadBundle(s.bundlePath)
	c.Assert(err, IsNil)

	path := filepath.Join(c.MkDir(), "charm")
	err = bundle.ExpandTo(path)
	c.Assert(err, IsNil)

	dir, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	checkDummy(c, dir, path)
}
Пример #8
0
func AddCustomCharm(c *C, st *State, name, filename, content, series string, revision int) *Charm {
	path := testing.Charms.ClonedDirPath(c.MkDir(), name)
	if filename != "" {
		config := filepath.Join(path, filename)
		err := ioutil.WriteFile(config, []byte(content), 0644)
		c.Assert(err, IsNil)
	}
	ch, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	if revision != -1 {
		ch.SetRevision(revision)
	}
	return addCharm(c, st, series, ch)
}
Пример #9
0
func (s *DirSuite) TestBundleToWithBadType(c *C) {
	charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
	badFile := filepath.Join(charmDir, "hooks", "badfile")

	// Symlink targeting a path outside of the charm.
	err := os.Symlink("../../target", badFile)
	c.Assert(err, IsNil)

	dir, err := charm.ReadDir(charmDir)
	c.Assert(err, IsNil)

	err = dir.BundleTo(&bytes.Buffer{})
	c.Assert(err, ErrorMatches, `symlink "hooks/badfile" links out of charm: "../../target"`)

	// Symlink targeting an absolute path.
	os.Remove(badFile)
	err = os.Symlink("/target", badFile)
	c.Assert(err, IsNil)

	dir, err = charm.ReadDir(charmDir)
	c.Assert(err, IsNil)

	err = dir.BundleTo(&bytes.Buffer{})
	c.Assert(err, ErrorMatches, `symlink "hooks/badfile" is absolute: "/target"`)

	// Can't bundle special files either.
	os.Remove(badFile)
	err = syscall.Mkfifo(badFile, 0644)
	c.Assert(err, IsNil)

	dir, err = charm.ReadDir(charmDir)
	c.Assert(err, IsNil)

	err = dir.BundleTo(&bytes.Buffer{})
	c.Assert(err, ErrorMatches, `file is a named pipe: "hooks/badfile"`)
}
Пример #10
0
func (s *UpgradeCharmSuccessSuite) TestUpgradesWithBundle(c *C) {
	dir, err := charm.ReadDir(s.path)
	c.Assert(err, IsNil)
	dir.SetRevision(42)
	buf := &bytes.Buffer{}
	err = dir.BundleTo(buf)
	c.Assert(err, IsNil)
	bundlePath := path.Join(s.SeriesPath, "riak.charm")
	err = ioutil.WriteFile(bundlePath, buf.Bytes(), 0644)
	c.Assert(err, IsNil)

	err = runUpgradeCharm(c, "riak")
	c.Assert(err, IsNil)
	s.assertUpgraded(c, 42, false)
	s.assertLocalRevision(c, 7, s.path)
}
Пример #11
0
func (s *BundleSuite) TestBundleSetRevision(c *C) {
	bundle, err := charm.ReadBundle(s.bundlePath)
	c.Assert(err, IsNil)

	c.Assert(bundle.Revision(), Equals, 1)
	bundle.SetRevision(42)
	c.Assert(bundle.Revision(), Equals, 42)

	path := filepath.Join(c.MkDir(), "charm")
	err = bundle.ExpandTo(path)
	c.Assert(err, IsNil)

	dir, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	c.Assert(dir.Revision(), Equals, 42)
}
Пример #12
0
func (s *DeployerSuite) bundle(c *C, customize func(path string)) *corecharm.Bundle {
	base := c.MkDir()
	dirpath := testing.Charms.ClonedDirPath(base, "dummy")
	customize(dirpath)
	dir, err := corecharm.ReadDir(dirpath)
	c.Assert(err, IsNil)
	bunpath := filepath.Join(base, "bundle")
	file, err := os.Create(bunpath)
	c.Assert(err, IsNil)
	defer file.Close()
	err = dir.BundleTo(file)
	c.Assert(err, IsNil)
	bun, err := corecharm.ReadBundle(bunpath)
	c.Assert(err, IsNil)
	return bun
}
Пример #13
0
func (s createCharm) step(c *C, ctx *context) {
	base := coretesting.Charms.ClonedDirPath(c.MkDir(), "series", "wordpress")
	for _, name := range charmHooks {
		path := filepath.Join(base, "hooks", name)
		good := true
		for _, bad := range s.badHooks {
			if name == bad {
				good = false
			}
		}
		ctx.writeHook(c, path, good)
	}
	if s.customize != nil {
		s.customize(c, base)
	}
	dir, err := charm.ReadDir(base)
	c.Assert(err, IsNil)
	err = dir.SetDiskRevision(s.revision)
	c.Assert(err, IsNil)
	step(c, ctx, addCharm{dir, curl(s.revision)})
}
Пример #14
0
func (s *BundleSuite) TestExpandToSetsHooksExecutable(c *C) {
	charmDir := testing.Charms.ClonedDir(c.MkDir(), "all-hooks")
	// Bundle manually, so we can check ExpandTo(), unaffected
	// by BundleTo()'s behavior
	bundlePath := filepath.Join(c.MkDir(), "bundle.charm")
	s.prepareBundle(c, charmDir, bundlePath)
	bundle, err := charm.ReadBundle(bundlePath)
	c.Assert(err, IsNil)

	path := filepath.Join(c.MkDir(), "charm")
	err = bundle.ExpandTo(path)
	c.Assert(err, IsNil)

	_, err = charm.ReadDir(path)
	c.Assert(err, IsNil)

	for name := range bundle.Meta().Hooks() {
		hookName := string(name)
		info, err := os.Stat(filepath.Join(path, "hooks", hookName))
		c.Assert(err, IsNil)
		perm := info.Mode() & 0777
		c.Assert(perm&0100 != 0, Equals, true, Commentf("hook %q is not executable", hookName))
	}
}
Пример #15
0
func (s *DirSuite) TestReadDir(c *C) {
	path := testing.Charms.DirPath("dummy")
	dir, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	checkDummy(c, dir, path)
}
Пример #16
0
func (c *PublishCommand) Run(ctx *cmd.Context) (err error) {
	branch := bzr.New(ctx.AbsPath(c.CharmPath))
	if _, err := os.Stat(branch.Join(".bzr")); err != nil {
		return fmt.Errorf("not a charm branch: %s", branch.Location())
	}
	if err := branch.CheckClean(); err != nil {
		return err
	}

	var curl *charm.URL
	if c.URL == "" {
		if err == nil {
			loc, err := branch.PushLocation()
			if err != nil {
				return fmt.Errorf("no charm URL provided and cannot infer from current directory (no push location)")
			}
			curl, err = charm.Store.CharmURL(loc)
			if err != nil {
				return fmt.Errorf("cannot infer charm URL from branch location: %q", loc)
			}
		}
	} else {
		curl, err = charm.InferURL(c.URL, "")
		if err != nil {
			return err
		}
	}

	pushLocation := charm.Store.BranchLocation(curl)
	if c.changePushLocation != nil {
		pushLocation = c.changePushLocation(pushLocation)
	}

	repo, err := charm.InferRepository(curl, "/not/important")
	if err != nil {
		return err
	}
	if repo != charm.Store {
		return fmt.Errorf("charm URL must reference the juju charm store")
	}

	localDigest, err := branch.RevisionId()
	if err != nil {
		return fmt.Errorf("cannot obtain local digest: %v", err)
	}
	log.Infof("local digest is %s", localDigest)

	ch, err := charm.ReadDir(branch.Location())
	if err != nil {
		return err
	}
	if ch.Meta().Name != curl.Name {
		return fmt.Errorf("charm name in metadata must match name in URL: %q != %q", ch.Meta().Name, curl.Name)
	}

	oldEvent, err := charm.Store.Event(curl, localDigest)
	if _, ok := err.(*charm.NotFoundError); ok {
		oldEvent, err = charm.Store.Event(curl, "")
		if _, ok := err.(*charm.NotFoundError); ok {
			log.Infof("charm %s is not yet in the store", curl)
			err = nil
		}
	}
	if err != nil {
		return fmt.Errorf("cannot obtain event details from the store: %s", err)
	}

	if oldEvent != nil && oldEvent.Digest == localDigest {
		return handleEvent(ctx, curl, oldEvent)
	}

	log.Infof("sending charm to the charm store...")

	err = branch.Push(&bzr.PushAttr{Location: pushLocation, Remember: true})
	if err != nil {
		return err
	}
	log.Infof("charm sent; waiting for it to be published...")
	for {
		time.Sleep(c.pollDelay)
		newEvent, err := charm.Store.Event(curl, "")
		if _, ok := err.(*charm.NotFoundError); ok {
			continue
		}
		if err != nil {
			return fmt.Errorf("cannot obtain event details from the store: %s", err)
		}
		if oldEvent != nil && oldEvent.Digest == newEvent.Digest {
			continue
		}
		if newEvent.Digest != localDigest {
			// TODO Check if the published digest is in the local history.
			return fmt.Errorf("charm changed but not to local charm digest; publishing race?")
		}
		return handleEvent(ctx, curl, newEvent)
	}
	return nil
}
Пример #17
0
// PublishBazaarBranch checks out the Bazaar branch from burl and
// publishes its latest revision at urls in the given store.
// The digest parameter must be the most recent known Bazaar
// revision id for the branch tip. If publishing this specific digest
// for these URLs has been attempted already, the publishing
// procedure may abort early. The published digest is the Bazaar
// revision id of the checked out branch's tip, though, which may
// differ from the digest parameter.
func PublishBazaarBranch(store *Store, urls []*charm.URL, burl string, digest string) error {

	// Prevent other publishers from updating these specific URLs
	// concurrently.
	lock, err := store.LockUpdates(urls)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	var branchDir string
NewTip:
	// Prepare the charm publisher. This will compute the revision
	// to be assigned to the charm, and it will also fail if the
	// operation is unnecessary because charms are up-to-date.
	pub, err := store.CharmPublisher(urls, digest)
	if err != nil {
		return err
	}

	// Figure if publishing this charm was already attempted before and
	// failed. We won't try again endlessly if so. In the future we may
	// retry automatically in certain circumstances.
	event, err := store.CharmEvent(urls[0], digest)
	if err == nil && event.Kind != EventPublished {
		return fmt.Errorf("charm publishing previously failed: %s", strings.Join(event.Errors, "; "))
	} else if err != nil && err != ErrNotFound {
		return err
	}

	if branchDir == "" {
		// Retrieve the branch with a lightweight checkout, so that it
		// builds a working tree as cheaply as possible. History
		// doesn't matter here.
		tempDir, err := ioutil.TempDir("", "publish-branch-")
		if err != nil {
			return err
		}
		defer os.RemoveAll(tempDir)
		branchDir = filepath.Join(tempDir, "branch")
		output, err := exec.Command("bzr", "checkout", "--lightweight", burl, branchDir).CombinedOutput()
		if err != nil {
			return outputErr(output, err)
		}

		// Pick actual digest from tip. Publishing the real tip
		// revision rather than the revision for the digest provided is
		// strictly necessary to prevent a race condition. If the
		// provided digest was published instead, there's a chance
		// another publisher concurrently running could have found a
		// newer revision and published that first, and the digest
		// parameter provided is in fact an old version that would
		// overwrite the new version.
		tipDigest, err := bzrRevisionId(branchDir)
		if err != nil {
			return err
		}
		if tipDigest != digest {
			digest = tipDigest
			goto NewTip
		}
	}

	ch, err := charm.ReadDir(branchDir)
	if err == nil {
		// Hand over the charm to the store for bundling and
		// streaming its content into the database.
		err = pub.Publish(ch)
		if err == ErrUpdateConflict {
			// A conflict may happen in edge cases if the whole
			// locking mechanism fails due to an expiration event,
			// and then the expired concurrent publisher revives
			// for whatever reason and attempts to finish
			// publishing. The state of the system is still
			// consistent in that case, and the error isn't logged
			// since the revision was properly published before.
			return err
		}
	}

	// Publishing is done. Log failure or error.
	event = &CharmEvent{
		URLs:   urls,
		Digest: digest,
	}
	if err == nil {
		event.Kind = EventPublished
		event.Revision = pub.Revision()
	} else {
		event.Kind = EventPublishError
		event.Errors = []string{err.Error()}
	}
	if logerr := store.LogCharmEvent(event); logerr != nil {
		if err == nil {
			err = logerr
		} else {
			err = fmt.Errorf("%v; %v", err, logerr)
		}
	}
	return err
}
Пример #18
0
// updateRelations responds to changes in the life states of the relations
// with the supplied ids. If any id corresponds to an alive relation not
// known to the unit, the uniter will join that relation and return its
// relationer in the added list.
func (u *Uniter) updateRelations(ids []int) (added []*Relationer, err error) {
	for _, id := range ids {
		if r, found := u.relationers[id]; found {
			rel := r.ru.Relation()
			if err := rel.Refresh(); err != nil {
				return nil, fmt.Errorf("cannot update relation %q: %v", rel, err)
			}
			if rel.Life() == state.Dying {
				if err := r.SetDying(); err != nil {
					return nil, err
				} else if r.IsImplicit() {
					delete(u.relationers, id)
				}
			}
			continue
		}
		// Relations that are not alive are simply skipped, because they
		// were not previously known anyway.
		rel, err := u.st.Relation(id)
		if err != nil {
			if errors.IsNotFoundError(err) {
				continue
			}
			return nil, err
		}
		if rel.Life() != state.Alive {
			continue
		}
		// Make sure we ignore relations not implemented by the unit's charm
		ch, err := corecharm.ReadDir(u.charm.Path())
		if err != nil {
			return nil, err
		}
		if ep, err := rel.Endpoint(u.unit.ServiceName()); err != nil {
			return nil, err
		} else if !ep.ImplementedBy(ch) {
			log.Warningf("worker/uniter: skipping relation with unknown endpoint %q", ep)
			continue
		}
		dir, err := relation.ReadStateDir(u.relationsDir, id)
		if err != nil {
			return nil, err
		}
		err = u.addRelation(rel, dir)
		if err == nil {
			added = append(added, u.relationers[id])
			continue
		}
		e := dir.Remove()
		if err != state.ErrCannotEnterScope {
			return nil, err
		}
		if e != nil {
			return nil, e
		}
	}
	if u.unit.IsPrincipal() {
		return added, nil
	}
	// If no Alive relations remain between a subordinate unit's service
	// and its principal's service, the subordinate must become Dying.
	keepAlive := false
	for _, r := range u.relationers {
		scope := r.ru.Endpoint().Scope
		if scope == corecharm.ScopeContainer && !r.dying {
			keepAlive = true
			break
		}
	}
	if !keepAlive {
		if err := u.unit.Destroy(); err != nil {
			return nil, err
		}
	}
	return added, nil
}
Пример #19
0
// Dir returns the actual charm.Dir named name.
func (r *Repo) Dir(name string) *charm.Dir {
	ch, err := charm.ReadDir(r.DirPath(name))
	check(err)
	return ch
}
Пример #20
0
func (s *DirSuite) TestBundleTo(c *C) {
	baseDir := c.MkDir()
	charmDir := testing.Charms.ClonedDirPath(baseDir, "dummy")
	var haveSymlinks = true
	if err := os.Symlink("../target", filepath.Join(charmDir, "hooks/symlink")); err != nil {
		haveSymlinks = false
	}
	dir, err := charm.ReadDir(charmDir)
	c.Assert(err, IsNil)
	path := filepath.Join(baseDir, "bundle.charm")
	file, err := os.Create(path)
	c.Assert(err, IsNil)
	err = dir.BundleTo(file)
	file.Close()
	c.Assert(err, IsNil)

	zipr, err := zip.OpenReader(path)
	c.Assert(err, IsNil)
	defer zipr.Close()

	var metaf, instf, emptyf, revf, symf *zip.File
	for _, f := range zipr.File {
		c.Logf("Bundled file: %s", f.Name)
		switch f.Name {
		case "revision":
			revf = f
		case "metadata.yaml":
			metaf = f
		case "hooks/install":
			instf = f
		case "hooks/symlink":
			symf = f
		case "empty/":
			emptyf = f
		case "build/ignored":
			c.Errorf("bundle includes build/*: %s", f.Name)
		case ".ignored", ".dir/ignored":
			c.Errorf("bundle includes .* entries: %s", f.Name)
		}
	}

	c.Assert(revf, NotNil)
	reader, err := revf.Open()
	c.Assert(err, IsNil)
	data, err := ioutil.ReadAll(reader)
	reader.Close()
	c.Assert(err, IsNil)
	c.Assert(string(data), Equals, "1")

	c.Assert(metaf, NotNil)
	reader, err = metaf.Open()
	c.Assert(err, IsNil)
	meta, err := charm.ReadMeta(reader)
	reader.Close()
	c.Assert(err, IsNil)
	c.Assert(meta.Name, Equals, "dummy")

	c.Assert(instf, NotNil)
	// Despite it being 0751, we pack and unpack it as 0755.
	c.Assert(instf.Mode()&0777, Equals, os.FileMode(0755))

	if haveSymlinks {
		c.Assert(symf, NotNil)
		c.Assert(symf.Mode()&0777, Equals, os.FileMode(0777))
		reader, err = symf.Open()
		c.Assert(err, IsNil)
		data, err = ioutil.ReadAll(reader)
		reader.Close()
		c.Assert(err, IsNil)
		c.Assert(string(data), Equals, "../target")
	} else {
		c.Assert(symf, IsNil)
	}

	c.Assert(emptyf, NotNil)
	c.Assert(emptyf.Mode()&os.ModeType, Equals, os.ModeDir)
	// Despite it being 0750, we pack and unpack it as 0755.
	c.Assert(emptyf.Mode()&0777, Equals, os.FileMode(0755))
}
Пример #21
0
func (s *UpgradeCharmSuccessSuite) assertLocalRevision(c *C, revision int, path string) {
	dir, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	c.Assert(dir.Revision(), Equals, revision)
}
Пример #22
0
// ClonedDir returns an actual charm.Dir based on a new copy of the charm directory
// named name, in the directory dst.
func (r *Repo) ClonedDir(dst, name string) *charm.Dir {
	ch, err := charm.ReadDir(r.ClonedDirPath(dst, name))
	check(err)
	return ch
}
Пример #23
0
func (s *ServiceSuite) TestEndpoints(c *C) {
	// Check state for charm with no explicit relations.
	eps, err := s.service.Endpoints()
	c.Assert(err, IsNil)
	jujuInfo := state.Endpoint{
		ServiceName:   "mysql",
		Interface:     "juju-info",
		RelationName:  "juju-info",
		RelationRole:  state.RoleProvider,
		RelationScope: charm.ScopeGlobal,
	}
	c.Assert(eps, DeepEquals, []state.Endpoint{jujuInfo})
	checkCommonNames := func() {
		ep, err := s.service.Endpoint("juju-info")
		c.Assert(err, IsNil)
		c.Assert(ep, DeepEquals, jujuInfo)

		_, err = s.service.Endpoint("voodoo-economy")
		c.Assert(err, ErrorMatches, `service "mysql" has no "voodoo-economy" relation`)
	}
	checkCommonNames()

	// Set a new charm, with a few relations.
	path := testing.Charms.ClonedDirPath(c.MkDir(), "series", "dummy")
	metaPath := filepath.Join(path, "metadata.yaml")
	f, err := os.OpenFile(metaPath, os.O_WRONLY|os.O_APPEND, 0644)
	c.Assert(err, IsNil)
	_, err = f.Write([]byte(`
provides:
  db: mysql
  db-admin: mysql
requires:
  foo:
    interface: bar
    scope: container
peers:
  pressure: pressure
`))
	f.Close()
	c.Assert(err, IsNil)
	ch, err := charm.ReadDir(path)
	c.Assert(err, IsNil)
	sch, err := s.State.AddCharm(
		// Fake everything; just use a different URL.
		ch, s.charm.URL().WithRevision(99), s.charm.BundleURL(), s.charm.BundleSha256(),
	)
	c.Assert(err, IsNil)
	err = s.service.SetCharm(sch, false)
	c.Assert(err, IsNil)

	// Check single endpoint.
	checkCommonNames()
	pressure := state.Endpoint{
		ServiceName:   "mysql",
		Interface:     "pressure",
		RelationName:  "pressure",
		RelationRole:  state.RolePeer,
		RelationScope: charm.ScopeGlobal,
	}
	ep, err := s.service.Endpoint("pressure")
	c.Assert(err, IsNil)
	c.Assert(ep, DeepEquals, pressure)

	// Check the full list of endpoints.
	eps, err = s.service.Endpoints()
	c.Assert(err, IsNil)
	c.Assert(eps, HasLen, 5)
	actual := map[string]state.Endpoint{}
	for _, ep := range eps {
		actual[ep.RelationName] = ep
	}
	c.Assert(actual, DeepEquals, map[string]state.Endpoint{
		"juju-info": jujuInfo,
		"pressure":  pressure,
		"db": {
			ServiceName:   "mysql",
			Interface:     "mysql",
			RelationName:  "db",
			RelationRole:  state.RoleProvider,
			RelationScope: charm.ScopeGlobal,
		},
		"db-admin": {
			ServiceName:   "mysql",
			Interface:     "mysql",
			RelationName:  "db-admin",
			RelationRole:  state.RoleProvider,
			RelationScope: charm.ScopeGlobal,
		},
		"foo": {
			ServiceName:   "mysql",
			Interface:     "bar",
			RelationName:  "foo",
			RelationRole:  state.RoleRequirer,
			RelationScope: charm.ScopeContainer,
		},
	})
}