// processPost handles a charm upload POST request after authentication. func (h *charmsHandler) processPost(r *http.Request, st *state.State) (*charm.URL, error) { query := r.URL.Query() schema := query.Get("schema") if schema == "" { schema = "local" } series := query.Get("series") if series != "" { if err := charm.ValidateSeries(series); err != nil { return nil, errors.NewBadRequest(err, "") } } // Make sure the content type is zip. contentType := r.Header.Get("Content-Type") if contentType != "application/zip" { return nil, errors.BadRequestf("expected Content-Type: application/zip, got: %v", contentType) } charmFileName, err := writeCharmToTempFile(r.Body) if err != nil { return nil, errors.Trace(err) } defer os.Remove(charmFileName) err = h.processUploadedArchive(charmFileName) if err != nil { return nil, err } archive, err := charm.ReadCharmArchive(charmFileName) if err != nil { return nil, errors.BadRequestf("invalid charm archive: %v", err) } name := archive.Meta().Name if err := charm.ValidateName(name); err != nil { return nil, errors.NewBadRequest(err, "") } // We got it, now let's reserve a charm URL for it in state. curl := &charm.URL{ Schema: schema, Name: archive.Meta().Name, Revision: archive.Revision(), Series: series, } if schema == "local" { curl, err = st.PrepareLocalCharmUpload(curl) if err != nil { return nil, errors.Trace(err) } } else { // "cs:" charms may only be uploaded into models which are // being imported during model migrations. There's currently // no other time where it makes sense to accept charm store // charms through this endpoint. if isImporting, err := modelIsImporting(st); err != nil { return nil, errors.Trace(err) } else if !isImporting { return nil, errors.New("cs charms may only be uploaded during model migration import") } // If a revision argument is provided, it takes precedence // over the revision in the charm archive. This is required to // handle the revision differences between unpublished and // published charms in the charm store. revisionStr := query.Get("revision") if revisionStr != "" { curl.Revision, err = strconv.Atoi(revisionStr) if err != nil { return nil, errors.NewBadRequest(errors.NewNotValid(err, "revision"), "") } } if _, err := st.PrepareStoreCharmUpload(curl); err != nil { return nil, errors.Trace(err) } } // Now we need to repackage it with the reserved URL, upload it to // provider storage and update the state. err = h.repackageAndUploadCharm(st, archive, curl) if err != nil { return nil, errors.Trace(err) } return curl, nil }
// 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, ) }
// 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, }, ) }