func addCharm(st *state.State, curl *charm.URL, ch charm.Charm) (*state.Charm, error) { var f *os.File name := charm.Quote(curl.String()) switch ch := ch.(type) { case *charm.Dir: var err error if f, err = ioutil.TempFile("", name); err != nil { return nil, err } defer os.Remove(f.Name()) defer f.Close() err = ch.BundleTo(f) if err != nil { return nil, fmt.Errorf("cannot bundle charm: %v", err) } if _, err := f.Seek(0, 0); err != nil { return nil, err } case *charm.Bundle: var err error if f, err = os.Open(ch.Path); err != nil { return nil, fmt.Errorf("cannot read charm bundle: %v", err) } defer f.Close() default: return nil, fmt.Errorf("unknown charm type %T", ch) } digest, size, err := utils.ReadSHA256(f) if err != nil { return nil, err } if _, err := f.Seek(0, 0); err != nil { return nil, err } cfg, err := st.EnvironConfig() if err != nil { return nil, err } env, err := environs.New(cfg) if err != nil { return nil, err } stor := env.Storage() if err := stor.Put(name, f, size); err != nil { return nil, fmt.Errorf("cannot put charm: %v", err) } ustr, err := stor.URL(name) if err != nil { return nil, fmt.Errorf("cannot get storage URL for charm: %v", err) } u, err := url.Parse(ustr) if err != nil { return nil, fmt.Errorf("cannot parse storage URL: %v", err) } sch, err := st.AddCharm(ch, curl, u, digest) if err != nil { return nil, fmt.Errorf("cannot add charm: %v", err) } return sch, nil }
// PutCharm uploads the given charm to provider storage, and adds a // state.Charm to the state. The charm is not uploaded if a charm with // the same URL already exists in the state. // If bumpRevision is true, the charm must be a local directory, // and the revision number will be incremented before pushing. func (conn *Conn) PutCharm(curl *charm.URL, repo charm.Repository, bumpRevision bool) (*state.Charm, error) { if curl.Revision == -1 { rev, err := charm.Latest(repo, curl) if err != nil { return nil, fmt.Errorf("cannot get latest charm revision: %v", err) } curl = curl.WithRevision(rev) } ch, err := repo.Get(curl) if err != nil { return nil, fmt.Errorf("cannot get charm: %v", err) } if bumpRevision { chd, ok := ch.(*charm.Dir) if !ok { return nil, fmt.Errorf("cannot increment revision of charm %q: not a directory", curl) } if err = chd.SetDiskRevision(chd.Revision() + 1); err != nil { return nil, fmt.Errorf("cannot increment revision of charm %q: %v", curl, err) } curl = curl.WithRevision(chd.Revision()) } if sch, err := conn.State.Charm(curl); err == nil { return sch, nil } return conn.addCharm(curl, ch) }
// AddCharm adds the ch charm with curl to the state. bundleURL must // be set to a URL where the bundle for ch may be downloaded from. On // success the newly added charm state is returned. func (st *State) AddCharm(ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string) (stch *Charm, err error) { // The charm may already exist in state as a placeholder, so we // check for that situation and update the existing charm record // if necessary, otherwise add a new record. var existing charmDoc err = st.charms.Find(bson.D{{"_id", curl.String()}, {"placeholder", true}}).One(&existing) if err == mgo.ErrNotFound { cdoc := &charmDoc{ URL: curl, Meta: ch.Meta(), Config: ch.Config(), Actions: ch.Actions(), BundleURL: bundleURL, BundleSha256: bundleSha256, } err = st.charms.Insert(cdoc) if err != nil { return nil, fmt.Errorf("cannot add charm %q: %v", curl, err) } return newCharm(st, cdoc) } else if err != nil { return nil, err } return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, stillPlaceholder) }
func (br *bundleReader) AddBundle(c *gc.C, url *corecharm.URL, bundle charm.Bundle) charm.BundleInfo { if br.bundles == nil { br.bundles = map[string]charm.Bundle{} } br.bundles[url.String()] = bundle return &bundleInfo{nil, url} }
// addCharmViaAPI calls the appropriate client API calls to add the // given charm URL to state. Also displays the charm URL of the added // charm on stdout. func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charm.Repository) (*charm.URL, error) { if curl.Revision < 0 { latest, err := charm.Latest(repo, curl) if err != nil { return nil, err } curl = curl.WithRevision(latest) } switch curl.Schema { case "local": ch, err := repo.Get(curl) if err != nil { return nil, err } stateCurl, err := client.AddLocalCharm(curl, ch) if err != nil { return nil, err } curl = stateCurl case "cs": err := client.AddCharm(curl) if err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) } ctx.Infof("Added charm %q to the environment.", curl) return curl, nil }
// DeleteCharm deletes the charms matching url. If no revision is specified, // all revisions of the charm are deleted. func (s *Store) DeleteCharm(url *charm.URL) ([]*CharmInfo, error) { logger.Debugf("deleting charm %s", url) infos, err := s.getRevisions(url, 0) if err != nil { return nil, err } if len(infos) == 0 { return nil, ErrNotFound } session := s.session.Copy() defer session.Close() var deleted []*CharmInfo for _, info := range infos { err := session.Charms().Remove( bson.D{{"urls", url.WithRevision(-1)}, {"revision", info.Revision()}}) if err != nil { logger.Errorf("failed to delete metadata for charm %s: %v", url, err) return deleted, err } err = session.CharmFS().RemoveId(info.fileId) if err != nil { logger.Errorf("failed to delete GridFS file for charm %s: %v", url, err) return deleted, err } deleted = append(deleted, info) } return deleted, err }
// storeManifest stores, into dataPath, the supplied manifest for the supplied charm. func (d *manifestDeployer) storeManifest(url *charm.URL, manifest set.Strings) error { if err := os.MkdirAll(d.DataPath(manifestsDataPath), 0755); err != nil { return err } name := charm.Quote(url.String()) path := filepath.Join(d.DataPath(manifestsDataPath), name) return utils.WriteYaml(path, manifest.SortedValues()) }
// PrepareLocalCharmUpload must be called before a local charm is // uploaded to the provider storage in order to create a charm // document in state. It returns the chosen unique charm URL reserved // in state for the charm. // // The url's schema must be "local" and it must include a revision. func (st *State) PrepareLocalCharmUpload(curl *charm.URL) (chosenUrl *charm.URL, err error) { // Perform a few sanity checks first. if curl.Schema != "local" { return nil, fmt.Errorf("expected charm URL with local schema, got %q", curl) } if curl.Revision < 0 { return nil, fmt.Errorf("expected charm URL with revision, got %q", curl) } // Get a regex with the charm URL and no revision. noRevURL := curl.WithRevision(-1) curlRegex := "^" + regexp.QuoteMeta(noRevURL.String()) for attempt := 0; attempt < 3; attempt++ { // Find the highest revision of that charm in state. var docs []charmDoc err = st.charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}}).Select(bson.D{{"_id", 1}}).All(&docs) if err != nil { return nil, err } // Find the highest revision. maxRevision := -1 for _, doc := range docs { if doc.URL.Revision > maxRevision { maxRevision = doc.URL.Revision } } // Respect the local charm's revision first. chosenRevision := curl.Revision if maxRevision >= chosenRevision { // More recent revision exists in state, pick the next. chosenRevision = maxRevision + 1 } chosenUrl = curl.WithRevision(chosenRevision) uploadedCharm := &charmDoc{ URL: chosenUrl, PendingUpload: true, } ops := []txn.Op{{ C: st.charms.Name, Id: uploadedCharm.URL, Assert: txn.DocMissing, Insert: uploadedCharm, }} // Run the transaction, and retry on abort. if err = st.runTransaction(ops); err == txn.ErrAborted { continue } else if err != nil { return nil, err } break } if err != nil { return nil, ErrExcessiveContention } return chosenUrl, nil }
// Charm returns the charm with the given URL. func (st *State) Charm(curl *charm.URL) (*Charm, error) { if curl == nil { return nil, fmt.Errorf("charm url cannot be nil") } return &Charm{ st: st, url: curl.String(), }, nil }
func handleEvent(ctx *cmd.Context, curl *charm.URL, event *charm.EventResponse) error { switch event.Kind { case "published": curlRev := curl.WithRevision(event.Revision) logger.Infof("charm published at %s as %s", event.Time, curlRev) fmt.Fprintln(ctx.Stdout, curlRev) case "publish-error": return fmt.Errorf("charm could not be published: %s", strings.Join(event.Errors, "; ")) default: return fmt.Errorf("unknown event kind %q for charm %s", event.Kind, curl) } return nil }
// AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which // must reference a charm from the store. The charm document is marked as a placeholder which // means that if the charm is to be deployed, it will need to first be uploaded to env storage. func (st *State) AddStoreCharmPlaceholder(curl *charm.URL) (err error) { // Perform sanity checks first. if curl.Schema != "cs" { return fmt.Errorf("expected charm URL with cs schema, got %q", curl) } if curl.Revision < 0 { return fmt.Errorf("expected charm URL with revision, got %q", curl) } for attempt := 0; attempt < 3; attempt++ { // See if the charm already exists in state and exit early if that's the case. var doc charmDoc err = st.charms.Find(bson.D{{"_id", curl.String()}}).Select(bson.D{{"_id", 1}}).One(&doc) if err != nil && err != mgo.ErrNotFound { return err } if err == nil { return nil } // Delete all previous placeholders so we don't fill up the database with unused data. var ops []txn.Op ops, err = st.deleteOldPlaceholderCharmsOps(curl) if err != nil { return nil } // Add the new charm doc. placeholderCharm := &charmDoc{ URL: curl, Placeholder: true, } ops = append(ops, txn.Op{ C: st.charms.Name, Id: placeholderCharm.URL.String(), Assert: txn.DocMissing, Insert: placeholderCharm, }) // Run the transaction, and retry on abort. if err = st.runTransaction(ops); err == txn.ErrAborted { continue } else if err != nil { return err } break } if err != nil { return ErrExcessiveContention } return nil }
// SetCharmURL marks the unit as currently using the supplied charm URL. // An error will be returned if the unit is dead, or the charm URL not known. func (u *Unit) SetCharmURL(curl *charm.URL) error { if curl == nil { return fmt.Errorf("charm URL cannot be nil") } var result params.ErrorResults args := params.EntitiesCharmURL{ Entities: []params.EntityCharmURL{ {Tag: u.tag.String(), CharmURL: curl.String()}, }, } err := u.st.call("SetCharmURL", args, &result) if err != nil { return err } return result.OneError() }
// serviceSetCharm1dot16 sets the charm for the given service in 1.16 // compatibility mode. Remove this when support for 1.16 is dropped. func (c *Client) serviceSetCharm1dot16(service *state.Service, curl *charm.URL, force bool) error { if curl.Schema != "cs" { return fmt.Errorf(`charm url has unsupported schema %q`, curl.Schema) } if curl.Revision < 0 { return fmt.Errorf("charm url must include revision") } err := c.AddCharm(params.CharmURL{curl.String()}) if err != nil { return err } ch, err := c.api.state.Charm(curl) if err != nil { return err } return service.SetCharm(ch, force) }
// LatestPlaceholderCharm returns the latest charm described by the // given URL but which is not yet deployed. func (st *State) LatestPlaceholderCharm(curl *charm.URL) (*Charm, error) { noRevURL := curl.WithRevision(-1) curlRegex := "^" + regexp.QuoteMeta(noRevURL.String()) var docs []charmDoc err := st.charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).All(&docs) if err != nil { return nil, fmt.Errorf("cannot get charm %q: %v", curl, err) } // Find the highest revision. var latest charmDoc for _, doc := range docs { if latest.URL == nil || doc.URL.Revision > latest.URL.Revision { latest = doc } } if latest.URL == nil { return nil, errors.NotFoundf("placeholder charm %q", noRevURL) } return newCharm(st, &latest) }
// AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which // must reference a charm from the store. The charm document is marked as a placeholder which // means that if the charm is to be deployed, it will need to first be uploaded to env storage. func (st *State) AddStoreCharmPlaceholder(curl *charm.URL) (err error) { // Perform sanity checks first. if curl.Schema != "cs" { return fmt.Errorf("expected charm URL with cs schema, got %q", curl) } if curl.Revision < 0 { return fmt.Errorf("expected charm URL with revision, got %q", curl) } buildTxn := func(attempt int) ([]txn.Op, error) { // See if the charm already exists in state and exit early if that's the case. var doc charmDoc err := st.charms.Find(bson.D{{"_id", curl.String()}}).Select(bson.D{{"_id", 1}}).One(&doc) if err != nil && err != mgo.ErrNotFound { return nil, err } if err == nil { return nil, jujutxn.ErrNoOperations } // Delete all previous placeholders so we don't fill up the database with unused data. ops, err := st.deleteOldPlaceholderCharmsOps(curl) if err != nil { return nil, err } // Add the new charm doc. placeholderCharm := &charmDoc{ URL: curl, Placeholder: true, } ops = append(ops, txn.Op{ C: st.charms.Name, Id: placeholderCharm.URL.String(), Assert: txn.DocMissing, Insert: placeholderCharm, }) return ops, nil } return st.run(buildTxn) }
// getRevisions returns at most the last n revisions for charm at url, // in descending revision order. For limit n=0, all revisions are returned. func (s *Store) getRevisions(url *charm.URL, n int) ([]*CharmInfo, error) { session := s.session.Copy() defer session.Close() logger.Debugf("retrieving charm info for %s", url) rev := url.Revision url = url.WithRevision(-1) charms := session.Charms() var cdocs []charmDoc var qdoc interface{} if rev == -1 { qdoc = bson.D{{"urls", url}} } else { qdoc = bson.D{{"urls", url}, {"revision", rev}} } q := charms.Find(qdoc).Sort("-revision") if n > 0 { q = q.Limit(n) } if err := q.All(&cdocs); err != nil { logger.Errorf("failed to find charm %s: %v", url, err) return nil, ErrNotFound } var infos []*CharmInfo for _, cdoc := range cdocs { infos = append(infos, &CharmInfo{ cdoc.Revision, cdoc.Digest, cdoc.Sha256, cdoc.Size, cdoc.FileId, cdoc.Meta, cdoc.Config, cdoc.Actions, }) } return infos, nil }
// deleteOldPlaceholderCharmsOps returns the txn ops required to delete all placeholder charm // records older than the specified charm URL. func (st *State) deleteOldPlaceholderCharmsOps(curl *charm.URL) ([]txn.Op, error) { // Get a regex with the charm URL and no revision. noRevURL := curl.WithRevision(-1) curlRegex := "^" + regexp.QuoteMeta(noRevURL.String()) var docs []charmDoc err := st.charms.Find( bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).Select(bson.D{{"_id", 1}}).All(&docs) if err != nil { return nil, err } var ops []txn.Op for _, doc := range docs { if doc.URL.Revision >= curl.Revision { continue } ops = append(ops, txn.Op{ C: st.charms.Name, Id: doc.URL.String(), Assert: stillPlaceholder, Remove: true, }) } return ops, nil }
// repackageAndUploadCharm expands the given charm archive to a // temporary directoy, repackages it with the given curl's revision, // then uploads it to providr storage, and finally updates the state. func (h *charmsHandler) repackageAndUploadCharm(archive *charm.Bundle, curl *charm.URL) error { // Create a temp dir to contain the extracted charm // dir and the repackaged archive. tempDir, err := ioutil.TempDir("", "charm-download") if err != nil { return errors.Annotate(err, "cannot create temp directory") } defer os.RemoveAll(tempDir) extractPath := filepath.Join(tempDir, "extracted") repackagedPath := filepath.Join(tempDir, "repackaged.zip") repackagedArchive, err := os.Create(repackagedPath) if err != nil { return errors.Annotate(err, "cannot repackage uploaded charm") } defer repackagedArchive.Close() // Expand and repack it with the revision specified by curl. archive.SetRevision(curl.Revision) if err := archive.ExpandTo(extractPath); err != nil { return errors.Annotate(err, "cannot extract uploaded charm") } charmDir, err := charm.ReadDir(extractPath) if err != nil { return errors.Annotate(err, "cannot read extracted charm") } // Bundle the charm and calculate its sha256 hash at the // same time. hash := sha256.New() err = charmDir.BundleTo(io.MultiWriter(hash, repackagedArchive)) if err != nil { return errors.Annotate(err, "cannot repackage uploaded charm") } bundleSHA256 := hex.EncodeToString(hash.Sum(nil)) size, err := repackagedArchive.Seek(0, 2) if err != nil { return errors.Annotate(err, "cannot get charm file size") } // Now upload to provider storage. if _, err := repackagedArchive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind the charm file reader") } storage, err := environs.GetStorage(h.state) if err != nil { return errors.Annotate(err, "cannot access provider storage") } name := charm.Quote(curl.String()) if err := storage.Put(name, repackagedArchive, size); err != nil { return errors.Annotate(err, "cannot upload charm to provider storage") } storageURL, err := storage.URL(name) if err != nil { return errors.Annotate(err, "cannot get storage URL for charm") } bundleURL, err := url.Parse(storageURL) if err != nil { return errors.Annotate(err, "cannot parse storage URL") } // And finally, update state. _, err = h.state.UpdateUploadedCharm(archive, curl, bundleURL, bundleSHA256) if err != nil { return errors.Annotate(err, "cannot update uploaded charm in state") } return nil }
func (s *LocalRepoSuite) checkNotFoundErr(c *gc.C, err error, charmURL *charm.URL) { expect := `charm not found in "` + s.repo.Path + `": ` + charmURL.String() c.Check(err, gc.ErrorMatches, expect) }
// WriteCharmURL writes a charm identity file into the supplied path. func WriteCharmURL(path string, url *charm.URL) error { return utils.WriteYaml(path, url.String()) }
// bundleURLPath returns the path to the location where the verified charm // bundle identified by url will be, or has been, saved. func (d *BundlesDir) bundleURLPath(url *charm.URL) string { return path.Join(d.path, charm.Quote(url.String())) }
// Run connects to the specified environment and starts the charm // upgrade process. func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error { client, err := juju.NewAPIClientFromName(c.EnvName) if err != nil { return err } defer client.Close() oldURL, err := client.ServiceGetCharmURL(c.ServiceName) if err != nil { return err } attrs, err := client.EnvironmentGet() if err != nil { return err } conf, err := config.New(config.NoDefaults, attrs) if err != nil { return err } var newURL *charm.URL if c.SwitchURL != "" { newURL, err = resolveCharmURL(c.SwitchURL, client, conf) if err != nil { return err } } else { // No new URL specified, but revision might have been. newURL = oldURL.WithRevision(c.Revision) } repo, err := charm.InferRepository(newURL.Reference, ctx.AbsPath(c.RepoPath)) if err != nil { return err } repo = config.SpecializeCharmRepo(repo, conf) // If no explicit revision was set with either SwitchURL // or Revision flags, discover the latest. explicitRevision := true if newURL.Revision == -1 { explicitRevision = false latest, err := charm.Latest(repo, newURL) if err != nil { return err } newURL = newURL.WithRevision(latest) } if *newURL == *oldURL { if explicitRevision { return fmt.Errorf("already running specified charm %q", newURL) } else if newURL.Schema == "cs" { // No point in trying to upgrade a charm store charm when // we just determined that's the latest revision // available. return fmt.Errorf("already running latest charm %q", newURL) } } addedURL, err := addCharmViaAPI(client, ctx, newURL, repo) if err != nil { return err } return client.ServiceSetCharm(c.ServiceName, addedURL.String(), c.Force) }
// AddLocalCharm prepares the given charm with a local: schema in its // URL, and uploads it via the API server, returning the assigned // charm URL. If the API server does not support charm uploads, an // error satisfying params.IsCodeNotImplemented() is returned. func (c *Client) AddLocalCharm(curl *charm.URL, ch charm.Charm) (*charm.URL, error) { if curl.Schema != "local" { return nil, fmt.Errorf("expected charm URL with local: schema, got %q", curl.String()) } // Package the charm for uploading. var archive *os.File switch ch := ch.(type) { case *charm.Dir: var err error if archive, err = ioutil.TempFile("", "charm"); err != nil { return nil, fmt.Errorf("cannot create temp file: %v", err) } defer os.Remove(archive.Name()) defer archive.Close() if err := ch.BundleTo(archive); err != nil { return nil, fmt.Errorf("cannot repackage charm: %v", err) } if _, err := archive.Seek(0, 0); err != nil { return nil, fmt.Errorf("cannot rewind packaged charm: %v", err) } case *charm.Bundle: var err error if archive, err = os.Open(ch.Path); err != nil { return nil, fmt.Errorf("cannot read charm archive: %v", err) } defer archive.Close() default: return nil, fmt.Errorf("unknown charm type %T", ch) } // Prepare the upload request. url := fmt.Sprintf("%s/charms?series=%s", c.st.serverRoot, curl.Series) req, err := http.NewRequest("POST", url, archive) if err != nil { return nil, fmt.Errorf("cannot create upload request: %v", err) } req.SetBasicAuth(c.st.tag, c.st.password) req.Header.Set("Content-Type", "application/zip") // Send the request. // BUG(dimitern) 2013-12-17 bug #1261780 // Due to issues with go 1.1.2, fixed later, we cannot use a // regular TLS client with the CACert here, because we get "x509: // cannot validate certificate for 127.0.0.1 because it doesn't // contain any IP SANs". Once we use a later go version, this // should be changed to connect to the API server with a regular // HTTP+TLS enabled client, using the CACert (possily cached, like // the tag and password) passed in api.Open()'s info argument. resp, err := utils.GetNonValidatingHTTPClient().Do(req) if err != nil { return nil, fmt.Errorf("cannot upload charm: %v", err) } if resp.StatusCode == http.StatusMethodNotAllowed { // API server is 1.16 or older, so charm upload // is not supported; notify the client. return nil, ¶ms.Error{ Message: "charm upload is not supported by the API server", Code: params.CodeNotImplemented, } } // Now parse the response & return. body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("cannot read charm upload response: %v", err) } defer resp.Body.Close() var jsonResponse params.CharmsResponse if err := json.Unmarshal(body, &jsonResponse); err != nil { return nil, fmt.Errorf("cannot unmarshal upload response: %v", err) } if jsonResponse.Error != "" { return nil, fmt.Errorf("error uploading charm: %v", jsonResponse.Error) } return charm.MustParseURL(jsonResponse.CharmURL), nil }
// AddCharm adds the given charm URL (which must include revision) to // the environment, if it does not exist yet. Local charms are not // supported, only charm store URLs. See also AddLocalCharm() in the // client-side API. func (c *Client) AddCharm(curl *charm.URL) error { args := params.CharmURL{URL: curl.String()} return c.call("AddCharm", args, nil) }