github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/apiserver/service/charmstore.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package service
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils"
    14  	"gopkg.in/juju/charm.v6-unstable"
    15  	"gopkg.in/juju/charmrepo.v1"
    16  	"gopkg.in/juju/charmrepo.v1/csclient"
    17  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    18  	"gopkg.in/macaroon.v1"
    19  
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/environs/config"
    22  	"github.com/juju/juju/state"
    23  )
    24  
    25  // TODO - we really want to avoid this, which we can do by refactoring code requiring this
    26  // to use interfaces.
    27  // NewCharmStore instantiates a new charm store repository.
    28  // It is defined at top level for testing purposes.
    29  var NewCharmStore = charmrepo.NewCharmStore
    30  
    31  // AddCharmWithAuthorization adds the given charm URL (which must include revision) to
    32  // the environment, if it does not exist yet. Local charms are not
    33  // supported, only charm store URLs. See also AddLocalCharm().
    34  //
    35  // The authorization macaroon, args.CharmStoreMacaroon, may be
    36  // omitted, in which case this call is equivalent to AddCharm.
    37  func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error {
    38  	charmURL, err := charm.ParseURL(args.URL)
    39  	if err != nil {
    40  		return err
    41  	}
    42  	if charmURL.Schema != "cs" {
    43  		return fmt.Errorf("only charm store charm URLs are supported, with cs: schema")
    44  	}
    45  	if charmURL.Revision < 0 {
    46  		return fmt.Errorf("charm URL must include revision")
    47  	}
    48  
    49  	// First, check if a pending or a real charm exists in state.
    50  	stateCharm, err := st.PrepareStoreCharmUpload(charmURL)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	if stateCharm.IsUploaded() {
    55  		// Charm already in state (it was uploaded already).
    56  		return nil
    57  	}
    58  
    59  	// Get the charm and its information from the store.
    60  	envConfig, err := st.EnvironConfig()
    61  	if err != nil {
    62  		return err
    63  	}
    64  	csURL, err := url.Parse(csclient.ServerURL)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	csParams := charmrepo.NewCharmStoreParams{
    69  		URL:        csURL.String(),
    70  		HTTPClient: httpbakery.NewHTTPClient(),
    71  	}
    72  	if args.CharmStoreMacaroon != nil {
    73  		// Set the provided charmstore authorizing macaroon
    74  		// as a cookie in the HTTP client.
    75  		// TODO discharge any third party caveats in the macaroon.
    76  		ms := []*macaroon.Macaroon{args.CharmStoreMacaroon}
    77  		httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms)
    78  	}
    79  	repo := config.SpecializeCharmRepo(
    80  		NewCharmStore(csParams),
    81  		envConfig,
    82  	)
    83  	downloadedCharm, err := repo.Get(charmURL)
    84  	if err != nil {
    85  		cause := errors.Cause(err)
    86  		if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) {
    87  			return errors.NewUnauthorized(err, "")
    88  		}
    89  		return errors.Trace(err)
    90  	}
    91  
    92  	// Open it and calculate the SHA256 hash.
    93  	downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive)
    94  	if !ok {
    95  		return errors.Errorf("expected a charm archive, got %T", downloadedCharm)
    96  	}
    97  	archive, err := os.Open(downloadedBundle.Path)
    98  	if err != nil {
    99  		return errors.Annotate(err, "cannot read downloaded charm")
   100  	}
   101  	defer archive.Close()
   102  	bundleSHA256, size, err := utils.ReadSHA256(archive)
   103  	if err != nil {
   104  		return errors.Annotate(err, "cannot calculate SHA256 hash of charm")
   105  	}
   106  	if _, err := archive.Seek(0, 0); err != nil {
   107  		return errors.Annotate(err, "cannot rewind charm archive")
   108  	}
   109  
   110  	// Store the charm archive in environment storage.
   111  	return StoreCharmArchive(
   112  		st,
   113  		charmURL,
   114  		downloadedCharm,
   115  		archive,
   116  		size,
   117  		bundleSHA256,
   118  	)
   119  }
   120  
   121  // StoreCharmArchive stores a charm archive in environment storage.
   122  func StoreCharmArchive(st *state.State, curl *charm.URL, ch charm.Charm, r io.Reader, size int64, sha256 string) error {
   123  	storage := newStateStorage(st.EnvironUUID(), st.MongoSession())
   124  	storagePath, err := charmArchiveStoragePath(curl)
   125  	if err != nil {
   126  		return errors.Annotate(err, "cannot generate charm archive name")
   127  	}
   128  	if err := storage.Put(storagePath, r, size); err != nil {
   129  		return errors.Annotate(err, "cannot add charm to storage")
   130  	}
   131  
   132  	// Now update the charm data in state and mark it as no longer pending.
   133  	_, err = st.UpdateUploadedCharm(ch, curl, storagePath, sha256)
   134  	if err != nil {
   135  		alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified ||
   136  			errors.Cause(err) == state.ErrCharmRevisionAlreadyModified ||
   137  			state.IsCharmAlreadyUploadedError(err)
   138  		if err := storage.Remove(storagePath); err != nil {
   139  			if alreadyUploaded {
   140  				logger.Errorf("cannot remove duplicated charm archive from storage: %v", err)
   141  			} else {
   142  				logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err)
   143  			}
   144  		}
   145  		if alreadyUploaded {
   146  			// Somebody else managed to upload and update the charm in
   147  			// state before us. This is not an error.
   148  			return nil
   149  		}
   150  	}
   151  	return nil
   152  }
   153  
   154  // charmArchiveStoragePath returns a string that is suitable as a
   155  // storage path, using a random UUID to avoid colliding with concurrent
   156  // uploads.
   157  func charmArchiveStoragePath(curl *charm.URL) (string, error) {
   158  	uuid, err := utils.NewUUID()
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  	return fmt.Sprintf("charms/%s-%s", curl.String(), uuid), nil
   163  }
   164  
   165  // ResolveCharm resolves the best available charm URLs with series, for charm
   166  // locations without a series specified.
   167  func ResolveCharms(st *state.State, args params.ResolveCharms) (params.ResolveCharmResults, error) {
   168  	var results params.ResolveCharmResults
   169  
   170  	envConfig, err := st.EnvironConfig()
   171  	if err != nil {
   172  		return params.ResolveCharmResults{}, err
   173  	}
   174  	repo := config.SpecializeCharmRepo(
   175  		NewCharmStore(charmrepo.NewCharmStoreParams{}),
   176  		envConfig)
   177  
   178  	for _, ref := range args.References {
   179  		result := params.ResolveCharmResult{}
   180  		curl, err := resolveCharm(&ref, repo)
   181  		if err != nil {
   182  			result.Error = err.Error()
   183  		} else {
   184  			result.URL = curl
   185  		}
   186  		results.URLs = append(results.URLs, result)
   187  	}
   188  	return results, nil
   189  }
   190  
   191  func resolveCharm(ref *charm.Reference, repo charmrepo.Interface) (*charm.URL, error) {
   192  	if ref.Schema != "cs" {
   193  		return nil, fmt.Errorf("only charm store charm references are supported, with cs: schema")
   194  	}
   195  
   196  	// Resolve the charm location with the repository.
   197  	curl, err := repo.Resolve(ref)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	return curl.WithRevision(ref.Revision), nil
   202  }