github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/application/charmstore.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils"
    14  	"github.com/juju/version"
    15  	"gopkg.in/juju/charm.v6-unstable"
    16  	"gopkg.in/juju/charmrepo.v2-unstable"
    17  	"gopkg.in/juju/charmrepo.v2-unstable/csclient"
    18  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    19  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    20  	"gopkg.in/macaroon.v1"
    21  
    22  	"github.com/juju/juju/apiserver/params"
    23  	"github.com/juju/juju/environs/config"
    24  	"github.com/juju/juju/state"
    25  	jujuversion "github.com/juju/juju/version"
    26  )
    27  
    28  // TODO - we really want to avoid this, which we can do by refactoring code requiring this
    29  // to use interfaces.
    30  // NewCharmStoreRepo instantiates a new charm store repository.
    31  // It is exported for testing purposes.
    32  var NewCharmStoreRepo = newCharmStoreFromClient
    33  
    34  func newCharmStoreFromClient(csClient *csclient.Client) charmrepo.Interface {
    35  	return charmrepo.NewCharmStoreFromClient(csClient)
    36  }
    37  
    38  // AddCharmWithAuthorization adds the given charm URL (which must include revision) to
    39  // the environment, if it does not exist yet. Local charms are not
    40  // supported, only charm store URLs. See also AddLocalCharm().
    41  //
    42  // The authorization macaroon, args.CharmStoreMacaroon, may be
    43  // omitted, in which case this call is equivalent to AddCharm.
    44  func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error {
    45  	charmURL, err := charm.ParseURL(args.URL)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	if charmURL.Schema != "cs" {
    50  		return fmt.Errorf("only charm store charm URLs are supported, with cs: schema")
    51  	}
    52  	if charmURL.Revision < 0 {
    53  		return fmt.Errorf("charm URL must include revision")
    54  	}
    55  
    56  	// First, check if a pending or a real charm exists in state.
    57  	stateCharm, err := st.PrepareStoreCharmUpload(charmURL)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	if stateCharm.IsUploaded() {
    62  		// Charm already in state (it was uploaded already).
    63  		return nil
    64  	}
    65  
    66  	// Open a charm store client.
    67  	repo, err := openCSRepo(args)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	modelConfig, err := st.ModelConfig()
    72  	if err != nil {
    73  		return err
    74  	}
    75  	repo = config.SpecializeCharmRepo(repo, modelConfig).(*charmrepo.CharmStore)
    76  
    77  	// Get the charm and its information from the store.
    78  	downloadedCharm, err := repo.Get(charmURL)
    79  	if err != nil {
    80  		cause := errors.Cause(err)
    81  		if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) {
    82  			return errors.NewUnauthorized(err, "")
    83  		}
    84  		return errors.Trace(err)
    85  	}
    86  
    87  	if err := checkMinVersion(downloadedCharm); err != nil {
    88  		return errors.Trace(err)
    89  	}
    90  
    91  	// Open it and calculate the SHA256 hash.
    92  	downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive)
    93  	if !ok {
    94  		return errors.Errorf("expected a charm archive, got %T", downloadedCharm)
    95  	}
    96  	archive, err := os.Open(downloadedBundle.Path)
    97  	if err != nil {
    98  		return errors.Annotate(err, "cannot read downloaded charm")
    99  	}
   100  	defer archive.Close()
   101  	bundleSHA256, size, err := utils.ReadSHA256(archive)
   102  	if err != nil {
   103  		return errors.Annotate(err, "cannot calculate SHA256 hash of charm")
   104  	}
   105  	if _, err := archive.Seek(0, 0); err != nil {
   106  		return errors.Annotate(err, "cannot rewind charm archive")
   107  	}
   108  
   109  	ca := CharmArchive{
   110  		ID:     charmURL,
   111  		Charm:  downloadedCharm,
   112  		Data:   archive,
   113  		Size:   size,
   114  		SHA256: bundleSHA256,
   115  	}
   116  	if args.CharmStoreMacaroon != nil {
   117  		ca.Macaroon = macaroon.Slice{args.CharmStoreMacaroon}
   118  	}
   119  
   120  	// Store the charm archive in environment storage.
   121  	return StoreCharmArchive(st, ca)
   122  }
   123  
   124  func openCSRepo(args params.AddCharmWithAuthorization) (charmrepo.Interface, error) {
   125  	csClient, err := openCSClient(args)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	repo := NewCharmStoreRepo(csClient)
   130  	return repo, nil
   131  }
   132  
   133  func openCSClient(args params.AddCharmWithAuthorization) (*csclient.Client, error) {
   134  	csURL, err := url.Parse(csclient.ServerURL)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	csParams := csclient.Params{
   139  		URL:        csURL.String(),
   140  		HTTPClient: httpbakery.NewHTTPClient(),
   141  	}
   142  
   143  	if args.CharmStoreMacaroon != nil {
   144  		// Set the provided charmstore authorizing macaroon
   145  		// as a cookie in the HTTP client.
   146  		// TODO(cmars) discharge any third party caveats in the macaroon.
   147  		ms := []*macaroon.Macaroon{args.CharmStoreMacaroon}
   148  		httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms)
   149  	}
   150  	csClient := csclient.New(csParams)
   151  	channel := csparams.Channel(args.Channel)
   152  	if channel != csparams.NoChannel {
   153  		csClient = csClient.WithChannel(channel)
   154  	}
   155  	return csClient, nil
   156  }
   157  
   158  func checkMinVersion(ch charm.Charm) error {
   159  	minver := ch.Meta().MinJujuVersion
   160  	if minver != version.Zero && minver.Compare(jujuversion.Current) > 0 {
   161  		return minVersionError(minver, jujuversion.Current)
   162  	}
   163  	return nil
   164  }
   165  
   166  type minJujuVersionErr struct {
   167  	*errors.Err
   168  }
   169  
   170  func minVersionError(minver, jujuver version.Number) error {
   171  	err := errors.NewErr("charm's min version (%s) is higher than this juju environment's version (%s)",
   172  		minver, jujuver)
   173  	err.SetLocation(1)
   174  	return minJujuVersionErr{&err}
   175  }
   176  
   177  // CharmArchive is the data that needs to be stored for a charm archive in
   178  // state.
   179  type CharmArchive struct {
   180  	// ID is the charm URL for which we're storing the archive.
   181  	ID *charm.URL
   182  
   183  	// Charm is the metadata about the charm for the archive.
   184  	Charm charm.Charm
   185  
   186  	// Data contains the bytes of the archive.
   187  	Data io.Reader
   188  
   189  	// Size is the number of bytes in Data.
   190  	Size int64
   191  
   192  	// SHA256 is the hash of the bytes in Data.
   193  	SHA256 string
   194  
   195  	// Macaroon is the authorization macaroon for accessing the charmstore.
   196  	Macaroon macaroon.Slice
   197  }
   198  
   199  // StoreCharmArchive stores a charm archive in environment storage.
   200  func StoreCharmArchive(st *state.State, archive CharmArchive) error {
   201  	storage := newStateStorage(st.ModelUUID(), st.MongoSession())
   202  	storagePath, err := charmArchiveStoragePath(archive.ID)
   203  	if err != nil {
   204  		return errors.Annotate(err, "cannot generate charm archive name")
   205  	}
   206  	if err := storage.Put(storagePath, archive.Data, archive.Size); err != nil {
   207  		return errors.Annotate(err, "cannot add charm to storage")
   208  	}
   209  
   210  	info := state.CharmInfo{
   211  		Charm:       archive.Charm,
   212  		ID:          archive.ID,
   213  		StoragePath: storagePath,
   214  		SHA256:      archive.SHA256,
   215  		Macaroon:    archive.Macaroon,
   216  	}
   217  
   218  	// Now update the charm data in state and mark it as no longer pending.
   219  	_, err = st.UpdateUploadedCharm(info)
   220  	if err != nil {
   221  		alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified ||
   222  			errors.Cause(err) == state.ErrCharmRevisionAlreadyModified ||
   223  			state.IsCharmAlreadyUploadedError(err)
   224  		if err := storage.Remove(storagePath); err != nil {
   225  			if alreadyUploaded {
   226  				logger.Errorf("cannot remove duplicated charm archive from storage: %v", err)
   227  			} else {
   228  				logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err)
   229  			}
   230  		}
   231  		if alreadyUploaded {
   232  			// Somebody else managed to upload and update the charm in
   233  			// state before us. This is not an error.
   234  			return nil
   235  		}
   236  	}
   237  	return nil
   238  }
   239  
   240  // charmArchiveStoragePath returns a string that is suitable as a
   241  // storage path, using a random UUID to avoid colliding with concurrent
   242  // uploads.
   243  func charmArchiveStoragePath(curl *charm.URL) (string, error) {
   244  	uuid, err := utils.NewUUID()
   245  	if err != nil {
   246  		return "", err
   247  	}
   248  	return fmt.Sprintf("charms/%s-%s", curl.String(), uuid), nil
   249  }
   250  
   251  // ResolveCharm resolves the best available charm URLs with series, for charm
   252  // locations without a series specified.
   253  func ResolveCharms(st *state.State, args params.ResolveCharms) (params.ResolveCharmResults, error) {
   254  	var results params.ResolveCharmResults
   255  
   256  	envConfig, err := st.ModelConfig()
   257  	if err != nil {
   258  		return params.ResolveCharmResults{}, err
   259  	}
   260  	repo := config.SpecializeCharmRepo(
   261  		NewCharmStoreRepo(csclient.New(csclient.Params{})),
   262  		envConfig)
   263  
   264  	for _, ref := range args.References {
   265  		result := params.ResolveCharmResult{}
   266  		curl, err := charm.ParseURL(ref)
   267  		if err != nil {
   268  			result.Error = err.Error()
   269  		} else {
   270  			curl, err := resolveCharm(curl, repo)
   271  			if err != nil {
   272  				result.Error = err.Error()
   273  			} else {
   274  				result.URL = curl.String()
   275  			}
   276  		}
   277  		results.URLs = append(results.URLs, result)
   278  	}
   279  	return results, nil
   280  }
   281  
   282  func resolveCharm(ref *charm.URL, repo charmrepo.Interface) (*charm.URL, error) {
   283  	if ref.Schema != "cs" {
   284  		return nil, fmt.Errorf("only charm store charm references are supported, with cs: schema")
   285  	}
   286  
   287  	// Resolve the charm location with the repository.
   288  	resolved, _, err := repo.Resolve(ref)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	if resolved.Series == "" {
   293  		return nil, errors.Errorf("no series found in charm URL %q", resolved)
   294  	}
   295  	return resolved.WithRevision(ref.Revision), nil
   296  }