// resolveCharmStoreEntityURL resolves the given charm or bundle URL string // by looking it up in the appropriate charm repository. // If it is a charm store URL, the given csParams will // be used to access the charm store repository. // If it is a local charm or bundle URL, the local charm repository at // the given repoPath will be used. The given configuration // will be used to add any necessary attributes to the repo // and to return the charm's supported series if possible. // // resolveCharmStoreEntityURL also returns the charm repository holding // the charm or bundle. func resolveCharmStoreEntityURL(args resolveCharmStoreEntityParams) (*charm.URL, []string, charmrepo.Interface, error) { url, err := charm.ParseURL(args.urlStr) if err != nil { return nil, nil, nil, errors.Trace(err) } repo, err := charmrepo.InferRepository(url, args.csParams, args.repoPath) if err != nil { return nil, nil, nil, errors.Trace(err) } repo = config.SpecializeCharmRepo(repo, args.conf) if url.Schema == "local" && url.Series == "" { if defaultSeries, ok := args.conf.DefaultSeries(); ok { url.Series = defaultSeries } if url.Series == "" { possibleURL := *url possibleURL.Series = config.LatestLtsSeries() logger.Errorf("The series is not specified in the model (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL) return nil, nil, nil, errors.Errorf("cannot resolve series for charm: %q", url) } } resultUrl, supportedSeries, err := repo.Resolve(url) if err != nil { return nil, nil, nil, errors.Trace(err) } return resultUrl, supportedSeries, repo, nil }
// ResolveCharm resolves the best available charm URLs with series, for charm // locations without a series specified. func ResolveCharms(st *state.State, args params.ResolveCharms) (params.ResolveCharmResults, error) { var results params.ResolveCharmResults envConfig, err := st.ModelConfig() if err != nil { return params.ResolveCharmResults{}, err } repo := config.SpecializeCharmRepo( NewCharmStoreRepo(csclient.New(csclient.Params{})), envConfig) for _, ref := range args.References { result := params.ResolveCharmResult{} curl, err := charm.ParseURL(ref) if err != nil { result.Error = err.Error() } else { curl, err := resolveCharm(curl, repo) if err != nil { result.Error = err.Error() } else { result.URL = curl.String() } } results.URLs = append(results.URLs, result) } return results, nil }
func (s *ConfigSuite) TestSpecializeCharmRepo(c *gc.C) { for i, test := range specializeCharmRepoTests { c.Logf("test %d: %s", i, test.about) cfg := newTestConfig(c, testing.Attrs{"test-mode": test.testMode}) repo := config.SpecializeCharmRepo(test.repo, cfg) store := repo.(*specializedCharmRepo) c.Assert(store.testMode, gc.Equals, test.testMode) } }
func (c *upgradeCharmCommand) getCharmStore( bakeryClient *httpbakery.Client, modelConfig *config.Config, ) *charmrepo.CharmStore { csClient := newCharmStoreClient(bakeryClient).WithChannel(c.Channel) return config.SpecializeCharmRepo( charmrepo.NewCharmStoreFromClient(csClient), modelConfig, ).(*charmrepo.CharmStore) }
// resolve resolves the given given charm or bundle URL // string by looking it up in the appropriate charm repository. If it is // a charm store URL, the given csParams will be used to access the // charm store repository. If it is a local charm or bundle URL, the // local charm repository at the given repoPath will be used. The given // configuration will be used to add any necessary attributes to the // repo and to return the charm's supported series if possible. // // It returns the fully resolved URL, any series supported by the entity, // and the repository that holds it. func (r *charmURLResolver) resolve(urlStr string) (*charm.URL, csparams.Channel, []string, charmrepo.Interface, error) { var noChannel csparams.Channel url, err := charm.ParseURL(urlStr) if err != nil { return nil, noChannel, nil, nil, errors.Trace(err) } switch url.Schema { case "cs": repo := config.SpecializeCharmRepo(r.csRepo, r.conf).(*charmrepo.CharmStore) resultUrl, channel, supportedSeries, err := repo.ResolveWithChannel(url) if err != nil { return nil, noChannel, nil, nil, errors.Trace(err) } return resultUrl, channel, supportedSeries, repo, nil case "local": if url.Series == "" { if defaultSeries, ok := r.conf.DefaultSeries(); ok { url.Series = defaultSeries } } if url.Series == "" { possibleURL := *url possibleURL.Series = config.LatestLtsSeries() logger.Errorf("The series is not specified in the model (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL) return nil, noChannel, nil, nil, errors.Errorf("cannot resolve series for charm: %q", url) } repo, err := charmrepo.NewLocalRepository(r.repoPath) if err != nil { return nil, noChannel, nil, nil, errors.Mask(err) } repo = config.SpecializeCharmRepo(repo, r.conf) resultUrl, supportedSeries, err := repo.Resolve(url) if err != nil { return nil, noChannel, nil, nil, errors.Trace(err) } return resultUrl, noChannel, supportedSeries, repo, nil default: return nil, noChannel, nil, nil, errors.Errorf("unknown schema for charm reference %q", urlStr) } }
// resolve resolves the given given charm or bundle URL // string by looking it up in the charm store. // The given csParams will be used to access the charm store. // // It returns the fully resolved URL, any series supported by the entity, // and the store that holds it. func (r *charmURLResolver) resolve(urlStr string) (*charm.URL, csparams.Channel, []string, *charmrepo.CharmStore, error) { var noChannel csparams.Channel url, err := charm.ParseURL(urlStr) if err != nil { return nil, noChannel, nil, nil, errors.Trace(err) } if url.Schema != "cs" { return nil, noChannel, nil, nil, errors.Errorf("unknown schema for charm URL %q", url) } charmStore := config.SpecializeCharmRepo(r.store, r.conf).(*charmrepo.CharmStore) resultUrl, channel, supportedSeries, err := charmStore.ResolveWithChannel(url) if err != nil { return nil, noChannel, nil, nil, errors.Trace(err) } return resultUrl, channel, supportedSeries, charmStore, nil }
func (c *Client) ResolveCharms(args params.ResolveCharms) (params.ResolveCharmResults, error) { var results params.ResolveCharmResults envConfig, err := c.api.state.EnvironConfig() if err != nil { return params.ResolveCharmResults{}, err } repo := config.SpecializeCharmRepo(CharmStore, envConfig) for _, ref := range args.References { result := params.ResolveCharmResult{} curl, err := c.resolveCharm(ref, repo) if err != nil { result.Error = err.Error() } else { result.URL = curl } results.URLs = append(results.URLs, result) } return results, nil }
// ResolveCharm resolves the best available charm URLs with series, for charm // locations without a series specified. func ResolveCharms(st *state.State, args params.ResolveCharms) (params.ResolveCharmResults, error) { var results params.ResolveCharmResults envConfig, err := st.EnvironConfig() if err != nil { return params.ResolveCharmResults{}, err } repo := config.SpecializeCharmRepo( NewCharmStore(charmrepo.NewCharmStoreParams{}), envConfig) for _, ref := range args.References { result := params.ResolveCharmResult{} curl, err := resolveCharm(&ref, repo) if err != nil { result.Error = err.Error() } else { result.URL = curl } results.URLs = append(results.URLs, result) } return results, nil }
// resolveCharmURL resolves the given charm URL string // by looking it up in the appropriate charm repository. // If it is a charm store charm URL, the given csParams will // be used to access the charm store repository. // If it is a local charm URL, the local charm repository at // the given repoPath will be used. The given configuration // will be used to add any necessary attributes to the repo // and to resolve the default series if possible. // // resolveCharmURL also returns the charm repository holding // the charm. func resolveCharmURL(curlStr string, csParams charmrepo.NewCharmStoreParams, repoPath string, conf *config.Config) (*charm.URL, charmrepo.Interface, error) { ref, err := charm.ParseReference(curlStr) if err != nil { return nil, nil, errors.Trace(err) } repo, err := charmrepo.InferRepository(ref, csParams, repoPath) if err != nil { return nil, nil, errors.Trace(err) } repo = config.SpecializeCharmRepo(repo, conf) if ref.Series == "" { if defaultSeries, ok := conf.DefaultSeries(); ok { ref.Series = defaultSeries } } if ref.Schema == "local" && ref.Series == "" { possibleURL := *ref possibleURL.Series = "trusty" logger.Errorf("The series is not specified in the environment (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL) return nil, nil, errors.Errorf("cannot resolve series for charm: %q", ref) } if ref.Series != "" && ref.Revision != -1 { // The URL is already fully resolved; do not // bother with an unnecessary round-trip to the // charm store. curl, err := ref.URL("") if err != nil { panic(err) } return curl, repo, nil } curl, err := repo.Resolve(ref) if err != nil { return nil, nil, errors.Trace(err) } return curl, repo, nil }
// Run connects to the specified environment and starts the charm // upgrade process. func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error { client, err := c.NewAPIClient() 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) }
// AddCharmWithAuthorization 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(). // // The authorization macaroon, args.CharmStoreMacaroon, may be // omitted, in which case this call is equivalent to AddCharm. func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error { charmURL, err := charm.ParseURL(args.URL) if err != nil { return err } if charmURL.Schema != "cs" { return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") } if charmURL.Revision < 0 { return fmt.Errorf("charm URL must include revision") } // First, check if a pending or a real charm exists in state. stateCharm, err := st.PrepareStoreCharmUpload(charmURL) if err != nil { return err } if stateCharm.IsUploaded() { // Charm already in state (it was uploaded already). return nil } // Get the charm and its information from the store. envConfig, err := st.EnvironConfig() if err != nil { return err } csURL, err := url.Parse(csclient.ServerURL) if err != nil { return err } csParams := charmrepo.NewCharmStoreParams{ URL: csURL.String(), HTTPClient: httpbakery.NewHTTPClient(), } if args.CharmStoreMacaroon != nil { // Set the provided charmstore authorizing macaroon // as a cookie in the HTTP client. // TODO discharge any third party caveats in the macaroon. ms := []*macaroon.Macaroon{args.CharmStoreMacaroon} httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms) } repo := config.SpecializeCharmRepo( NewCharmStore(csParams), envConfig, ) downloadedCharm, err := repo.Get(charmURL) if err != nil { cause := errors.Cause(err) if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { return errors.NewUnauthorized(err, "") } return errors.Trace(err) } // Open it and calculate the SHA256 hash. downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) if !ok { return errors.Errorf("expected a charm archive, got %T", downloadedCharm) } archive, err := os.Open(downloadedBundle.Path) if err != nil { return errors.Annotate(err, "cannot read downloaded charm") } defer archive.Close() bundleSHA256, size, err := utils.ReadSHA256(archive) if err != nil { return errors.Annotate(err, "cannot calculate SHA256 hash of charm") } if _, err := archive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind charm archive") } // Store the charm archive in environment storage. return StoreCharmArchive( st, charmURL, downloadedCharm, archive, size, bundleSHA256, ) }
// 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(). func (c *Client) AddCharm(args params.CharmURL) error { charmURL, err := charm.ParseURL(args.URL) if err != nil { return err } if charmURL.Schema != "cs" { return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") } if charmURL.Revision < 0 { return fmt.Errorf("charm URL must include revision") } // First, check if a pending or a real charm exists in state. stateCharm, err := c.api.state.PrepareStoreCharmUpload(charmURL) if err == nil && stateCharm.IsUploaded() { // Charm already in state (it was uploaded already). return nil } else if err != nil { return err } // Get the charm and its information from the store. envConfig, err := c.api.state.EnvironConfig() if err != nil { return err } store := config.SpecializeCharmRepo(CharmStore, envConfig) downloadedCharm, err := store.Get(charmURL) if err != nil { return errors.Annotatef(err, "cannot download charm %q", charmURL.String()) } // Open it and calculate the SHA256 hash. downloadedBundle, ok := downloadedCharm.(*charm.Bundle) if !ok { return errors.Errorf("expected a charm archive, got %T", downloadedCharm) } archive, err := os.Open(downloadedBundle.Path) if err != nil { return errors.Annotate(err, "cannot read downloaded charm") } defer archive.Close() bundleSHA256, size, err := utils.ReadSHA256(archive) if err != nil { return errors.Annotate(err, "cannot calculate SHA256 hash of charm") } if _, err := archive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind charm archive") } // Get the environment storage and upload the charm. env, err := environs.New(envConfig) if err != nil { return errors.Annotate(err, "cannot access environment") } storage := env.Storage() archiveName, err := CharmArchiveName(charmURL.Name, charmURL.Revision) if err != nil { return errors.Annotate(err, "cannot generate charm archive name") } if err := storage.Put(archiveName, archive, size); err != nil { return errors.Annotate(err, "cannot upload charm to provider storage") } storageURL, err := storage.URL(archiveName) 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") } // Finally, update the charm data in state and mark it as no longer pending. _, err = c.api.state.UpdateUploadedCharm(downloadedCharm, charmURL, bundleURL, bundleSHA256) if err == state.ErrCharmRevisionAlreadyModified || state.IsCharmAlreadyUploadedError(err) { // This is not an error, it just signifies somebody else // managed to upload and update the charm in state before // us. This means we have to delete what we just uploaded // to storage. if err := storage.Remove(archiveName); err != nil { errors.Annotate(err, "cannot remove duplicated charm from storage") } return nil } return err }
// AddCharmWithAuthorization 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(). // // The authorization macaroon, args.CharmStoreMacaroon, may be // omitted, in which case this call is equivalent to AddCharm. func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error { charmURL, err := charm.ParseURL(args.URL) if err != nil { return err } if charmURL.Schema != "cs" { return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") } if charmURL.Revision < 0 { return fmt.Errorf("charm URL must include revision") } // First, check if a pending or a real charm exists in state. stateCharm, err := st.PrepareStoreCharmUpload(charmURL) if err != nil { return err } if stateCharm.IsUploaded() { // Charm already in state (it was uploaded already). return nil } // Open a charm store client. repo, err := openCSRepo(args) if err != nil { return err } envConfig, err := st.ModelConfig() if err != nil { return err } repo = config.SpecializeCharmRepo(repo, envConfig).(*charmrepo.CharmStore) // Get the charm and its information from the store. downloadedCharm, err := repo.Get(charmURL) if err != nil { cause := errors.Cause(err) if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { return errors.NewUnauthorized(err, "") } return errors.Trace(err) } if err := checkMinVersion(downloadedCharm); err != nil { return errors.Trace(err) } // Open it and calculate the SHA256 hash. downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) if !ok { return errors.Errorf("expected a charm archive, got %T", downloadedCharm) } archive, err := os.Open(downloadedBundle.Path) if err != nil { return errors.Annotate(err, "cannot read downloaded charm") } defer archive.Close() bundleSHA256, size, err := utils.ReadSHA256(archive) if err != nil { return errors.Annotate(err, "cannot calculate SHA256 hash of charm") } if _, err := archive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind charm archive") } // Store the charm archive in environment storage. return StoreCharmArchive( st, CharmArchive{ ID: charmURL, Charm: downloadedCharm, Data: archive, Size: size, SHA256: bundleSHA256, }, ) }
func (c *DeployCommand) Run(ctx *cmd.Context) error { client, err := juju.NewAPIClientFromName(c.EnvName) if err != nil { return err } defer client.Close() attrs, err := client.EnvironmentGet() if err != nil { return err } conf, err := config.New(config.NoDefaults, attrs) if err != nil { return err } curl, err := resolveCharmURL(c.CharmName, client, conf) if err != nil { return err } repo, err := charm.InferRepository(curl.Reference, ctx.AbsPath(c.RepoPath)) if err != nil { return err } repo = config.SpecializeCharmRepo(repo, conf) curl, err = addCharmViaAPI(client, ctx, curl, repo) if err != nil { return err } if c.BumpRevision { ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.") } requestedNetworks, err := networkNamesToTags(parseNetworks(c.Networks)) if err != nil { return err } // We need to ensure network names are valid below, but we don't need them here. _, err = networkNamesToTags(c.Constraints.IncludeNetworks()) if err != nil { return err } _, err = networkNamesToTags(c.Constraints.ExcludeNetworks()) if err != nil { return err } haveNetworks := len(requestedNetworks) > 0 || c.Constraints.HaveNetworks() charmInfo, err := client.CharmInfo(curl.String()) if err != nil { return err } numUnits := c.NumUnits if charmInfo.Meta.Subordinate { if !constraints.IsEmpty(&c.Constraints) { return errors.New("cannot use --constraints with subordinate service") } if numUnits == 1 && c.ToMachineSpec == "" { numUnits = 0 } else { return errors.New("cannot use --num-units or --to with subordinate service") } } serviceName := c.ServiceName if serviceName == "" { serviceName = charmInfo.Meta.Name } var configYAML []byte if c.Config.Path != "" { configYAML, err = c.Config.Read(ctx) if err != nil { return err } } err = client.ServiceDeployWithNetworks( curl.String(), serviceName, numUnits, string(configYAML), c.Constraints, c.ToMachineSpec, requestedNetworks, ) if params.IsCodeNotImplemented(err) { if haveNetworks { return errors.New("cannot use --networks/--constraints networks=...: not supported by the API server") } err = client.ServiceDeploy( curl.String(), serviceName, numUnits, string(configYAML), c.Constraints, c.ToMachineSpec) } return err }