github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/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"
    16  	"gopkg.in/juju/charmrepo.v3"
    17  	"gopkg.in/juju/charmrepo.v3/csclient"
    18  	csparams "gopkg.in/juju/charmrepo.v3/csclient/params"
    19  	"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
    20  	"gopkg.in/macaroon.v2-unstable"
    21  
    22  	"github.com/juju/juju/apiserver/params"
    23  	"github.com/juju/juju/controller"
    24  	"github.com/juju/juju/core/lxdprofile"
    25  	"github.com/juju/juju/environs/config"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/state/storage"
    28  	jujuversion "github.com/juju/juju/version"
    29  )
    30  
    31  //go:generate mockgen -package mocks -destination mocks/storage_mock.go github.com/juju/juju/state/storage Storage
    32  //go:generate mockgen -package mocks -destination mocks/interface_mock.go gopkg.in/juju/charmrepo.v3 Interface
    33  //go:generate mockgen -package mocks -destination mocks/charm_mock.go github.com/juju/juju/apiserver/facades/client/application StateCharm
    34  //go:generate mockgen -package mocks -destination mocks/model_mock.go github.com/juju/juju/apiserver/facades/client/application StateModel
    35  //go:generate mockgen -package mocks -destination mocks/charmstore_mock.go github.com/juju/juju/apiserver/facades/client/application State
    36  
    37  // TODO - we really want to avoid this, which we can do by refactoring code requiring this
    38  // to use interfaces.
    39  
    40  // NewCharmStoreRepo instantiates a new charm store repository.
    41  // It is exported for testing purposes.
    42  var NewCharmStoreRepo = newCharmStoreFromClient
    43  
    44  var newStateStorage = storage.NewStorage
    45  
    46  func newCharmStoreFromClient(csClient *csclient.Client) charmrepo.Interface {
    47  	return charmrepo.NewCharmStoreFromClient(csClient)
    48  }
    49  
    50  // StateCharm represents a Charm from the state package
    51  type StateCharm interface {
    52  	IsUploaded() bool
    53  }
    54  
    55  // StateModel represents a Model from the state package
    56  type StateModel interface {
    57  	ModelConfig() (*config.Config, error)
    58  }
    59  
    60  // CharmState represents directives for accessing charm methods
    61  type CharmState interface {
    62  	UpdateUploadedCharm(info state.CharmInfo) (*state.Charm, error)
    63  	PrepareStoreCharmUpload(curl *charm.URL) (StateCharm, error)
    64  }
    65  
    66  // ModelState represents methods for accessing model definitions
    67  type ModelState interface {
    68  	Model() (StateModel, error)
    69  	ModelUUID() string
    70  }
    71  
    72  // ControllerState represents information defined for accessing controller
    73  // configuration
    74  type ControllerState interface {
    75  	ControllerConfig() (controller.Config, error)
    76  }
    77  
    78  // State represents the access patterns for the charm store methods.
    79  type State interface {
    80  	CharmState
    81  	ModelState
    82  	ControllerState
    83  	state.MongoSessioner
    84  }
    85  
    86  // AddCharmWithAuthorizationAndRepo adds the given charm URL (which must include
    87  // revision) to the environment, if it does not exist yet.
    88  // Local charms are not supported, only charm store URLs.
    89  // See also AddLocalCharm().
    90  // Additionally a Repo (See charmrepo.Interface) function factory can be
    91  // provided to help with overriding the source of downloading charms. The main
    92  // benefit of this indirection is to help with testing (mocking)
    93  //
    94  // The authorization macaroon, args.CharmStoreMacaroon, may be
    95  // omitted, in which case this call is equivalent to AddCharm.
    96  func AddCharmWithAuthorizationAndRepo(st State, args params.AddCharmWithAuthorization, repoFn func() (charmrepo.Interface, error)) error {
    97  	charmURL, err := charm.ParseURL(args.URL)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	if charmURL.Schema != "cs" {
   102  		return fmt.Errorf("only charm store charm URLs are supported, with cs: schema")
   103  	}
   104  	if charmURL.Revision < 0 {
   105  		return fmt.Errorf("charm URL must include revision")
   106  	}
   107  
   108  	// First, check if a pending or a real charm exists in state.
   109  	stateCharm, err := st.PrepareStoreCharmUpload(charmURL)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	if stateCharm.IsUploaded() {
   114  		// Charm already in state (it was uploaded already).
   115  		return nil
   116  	}
   117  
   118  	// Get the repo from the constructor
   119  	repo, err := repoFn()
   120  
   121  	// Get the charm and its information from the store.
   122  	downloadedCharm, err := repo.Get(charmURL)
   123  	if err != nil {
   124  		cause := errors.Cause(err)
   125  		if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) {
   126  			return errors.NewUnauthorized(err, "")
   127  		}
   128  		return errors.Trace(err)
   129  	}
   130  
   131  	if err := checkMinVersion(downloadedCharm); err != nil {
   132  		return errors.Trace(err)
   133  	}
   134  
   135  	// Open it and calculate the SHA256 hash.
   136  	downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive)
   137  	if !ok {
   138  		return errors.Errorf("expected a charm archive, got %T", downloadedCharm)
   139  	}
   140  
   141  	// Validate the charm lxd profile once we've downloaded it.
   142  	if err := lxdprofile.ValidateCharmLXDProfile(downloadedCharm); err != nil {
   143  		if !args.Force {
   144  			return errors.Annotate(err, "cannot add charm")
   145  		}
   146  	}
   147  
   148  	// Clean up the downloaded charm - we don't need to cache it in
   149  	// the filesystem as well as in blob storage.
   150  	defer os.Remove(downloadedBundle.Path)
   151  
   152  	archive, err := os.Open(downloadedBundle.Path)
   153  	if err != nil {
   154  		return errors.Annotate(err, "cannot read downloaded charm")
   155  	}
   156  	defer archive.Close()
   157  	bundleSHA256, size, err := utils.ReadSHA256(archive)
   158  	if err != nil {
   159  		return errors.Annotate(err, "cannot calculate SHA256 hash of charm")
   160  	}
   161  	if _, err := archive.Seek(0, 0); err != nil {
   162  		return errors.Annotate(err, "cannot rewind charm archive")
   163  	}
   164  
   165  	ca := CharmArchive{
   166  		ID:           charmURL,
   167  		Charm:        downloadedCharm,
   168  		Data:         archive,
   169  		Size:         size,
   170  		SHA256:       bundleSHA256,
   171  		CharmVersion: downloadedBundle.Version(),
   172  	}
   173  	if args.CharmStoreMacaroon != nil {
   174  		ca.Macaroon = macaroon.Slice{args.CharmStoreMacaroon}
   175  	}
   176  
   177  	// Store the charm archive in environment storage.
   178  	return StoreCharmArchive(st, ca)
   179  }
   180  
   181  // AddCharmWithAuthorization adds the given charm URL (which must include revision) to
   182  // the environment, if it does not exist yet. Local charms are not
   183  // supported, only charm store URLs. See also AddLocalCharm().
   184  //
   185  // The authorization macaroon, args.CharmStoreMacaroon, may be
   186  // omitted, in which case this call is equivalent to AddCharm.
   187  func AddCharmWithAuthorization(st State, args params.AddCharmWithAuthorization) error {
   188  	return AddCharmWithAuthorizationAndRepo(st, args, func() (charmrepo.Interface, error) {
   189  		// determine which charmstore api url to use.
   190  		controllerCfg, err := st.ControllerConfig()
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  
   195  		repo, err := openCSRepo(controllerCfg.CharmStoreURL(), args)
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  		model, err := st.Model()
   200  		if err != nil {
   201  			return nil, errors.Trace(err)
   202  		}
   203  		modelConfig, err := model.ModelConfig()
   204  		if err != nil {
   205  			return nil, errors.Trace(err)
   206  		}
   207  		repo = config.SpecializeCharmRepo(repo, modelConfig).(*charmrepo.CharmStore)
   208  		return repo, nil
   209  	})
   210  }
   211  
   212  func openCSRepo(csURL string, args params.AddCharmWithAuthorization) (charmrepo.Interface, error) {
   213  	csClient, err := openCSClient(csURL, args)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	repo := NewCharmStoreRepo(csClient)
   218  	return repo, nil
   219  }
   220  
   221  func openCSClient(csAPIURL string, args params.AddCharmWithAuthorization) (*csclient.Client, error) {
   222  	csURL, err := url.Parse(csAPIURL)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	csParams := csclient.Params{
   227  		URL:        csURL.String(),
   228  		HTTPClient: httpbakery.NewHTTPClient(),
   229  	}
   230  
   231  	if args.CharmStoreMacaroon != nil {
   232  		// Set the provided charmstore authorizing macaroon
   233  		// as a cookie in the HTTP client.
   234  		// TODO(cmars) discharge any third party caveats in the macaroon.
   235  		ms := []*macaroon.Macaroon{args.CharmStoreMacaroon}
   236  		httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms)
   237  	}
   238  	csClient := csclient.New(csParams)
   239  	channel := csparams.Channel(args.Channel)
   240  	if channel != csparams.NoChannel {
   241  		csClient = csClient.WithChannel(channel)
   242  	}
   243  	return csClient, nil
   244  }
   245  
   246  func checkMinVersion(ch charm.Charm) error {
   247  	minver := ch.Meta().MinJujuVersion
   248  	if minver != version.Zero && minver.Compare(jujuversion.Current) > 0 {
   249  		return minVersionError(minver, jujuversion.Current)
   250  	}
   251  	return nil
   252  }
   253  
   254  type minJujuVersionErr struct {
   255  	*errors.Err
   256  }
   257  
   258  func minVersionError(minver, jujuver version.Number) error {
   259  	err := errors.NewErr("charm's min version (%s) is higher than this juju model's version (%s)",
   260  		minver, jujuver)
   261  	err.SetLocation(1)
   262  	return minJujuVersionErr{&err}
   263  }
   264  
   265  // CharmArchive is the data that needs to be stored for a charm archive in
   266  // state.
   267  type CharmArchive struct {
   268  	// ID is the charm URL for which we're storing the archive.
   269  	ID *charm.URL
   270  
   271  	// Charm is the metadata about the charm for the archive.
   272  	Charm charm.Charm
   273  
   274  	// Data contains the bytes of the archive.
   275  	Data io.Reader
   276  
   277  	// Size is the number of bytes in Data.
   278  	Size int64
   279  
   280  	// SHA256 is the hash of the bytes in Data.
   281  	SHA256 string
   282  
   283  	// Macaroon is the authorization macaroon for accessing the charmstore.
   284  	Macaroon macaroon.Slice
   285  
   286  	// Charm Version contains semantic version of charm, typically the output of git describe.
   287  	CharmVersion string
   288  }
   289  
   290  // StoreCharmArchive stores a charm archive in environment storage.
   291  func StoreCharmArchive(st State, archive CharmArchive) error {
   292  	storage := newStateStorage(st.ModelUUID(), st.MongoSession())
   293  	storagePath, err := charmArchiveStoragePath(archive.ID)
   294  	if err != nil {
   295  		return errors.Annotate(err, "cannot generate charm archive name")
   296  	}
   297  	if err := storage.Put(storagePath, archive.Data, archive.Size); err != nil {
   298  		return errors.Annotate(err, "cannot add charm to storage")
   299  	}
   300  
   301  	info := state.CharmInfo{
   302  		Charm:       archive.Charm,
   303  		ID:          archive.ID,
   304  		StoragePath: storagePath,
   305  		SHA256:      archive.SHA256,
   306  		Macaroon:    archive.Macaroon,
   307  		Version:     archive.CharmVersion,
   308  	}
   309  
   310  	// Now update the charm data in state and mark it as no longer pending.
   311  	_, err = st.UpdateUploadedCharm(info)
   312  	if err != nil {
   313  		alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified ||
   314  			errors.Cause(err) == state.ErrCharmRevisionAlreadyModified ||
   315  			state.IsCharmAlreadyUploadedError(err)
   316  		if err := storage.Remove(storagePath); err != nil {
   317  			if alreadyUploaded {
   318  				logger.Errorf("cannot remove duplicated charm archive from storage: %v", err)
   319  			} else {
   320  				logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err)
   321  			}
   322  		}
   323  		if alreadyUploaded {
   324  			// Somebody else managed to upload and update the charm in
   325  			// state before us. This is not an error.
   326  			return nil
   327  		}
   328  		return errors.Trace(err)
   329  	}
   330  	return nil
   331  }
   332  
   333  // charmArchiveStoragePath returns a string that is suitable as a
   334  // storage path, using a random UUID to avoid colliding with concurrent
   335  // uploads.
   336  func charmArchiveStoragePath(curl *charm.URL) (string, error) {
   337  	uuid, err := utils.NewUUID()
   338  	if err != nil {
   339  		return "", err
   340  	}
   341  	return fmt.Sprintf("charms/%s-%s", curl.String(), uuid), nil
   342  }
   343  
   344  // ResolveCharm resolves the best available charm URLs with series, for charm
   345  // locations without a series specified.
   346  func ResolveCharms(st State, args params.ResolveCharms) (params.ResolveCharmResults, error) {
   347  	var results params.ResolveCharmResults
   348  
   349  	model, err := st.Model()
   350  	if err != nil {
   351  		return params.ResolveCharmResults{}, errors.Trace(err)
   352  	}
   353  	envConfig, err := model.ModelConfig()
   354  	if err != nil {
   355  		return params.ResolveCharmResults{}, err
   356  	}
   357  	controllerCfg, err := st.ControllerConfig()
   358  	if err != nil {
   359  		return params.ResolveCharmResults{}, err
   360  	}
   361  	csParams := csclient.Params{
   362  		URL: controllerCfg.CharmStoreURL(),
   363  	}
   364  	repo := config.SpecializeCharmRepo(
   365  		NewCharmStoreRepo(csclient.New(csParams)),
   366  		envConfig)
   367  
   368  	for _, ref := range args.References {
   369  		result := params.ResolveCharmResult{}
   370  		curl, err := charm.ParseURL(ref)
   371  		if err != nil {
   372  			result.Error = err.Error()
   373  		} else {
   374  			curl, err := resolveCharm(curl, repo)
   375  			if err != nil {
   376  				result.Error = err.Error()
   377  			} else {
   378  				result.URL = curl.String()
   379  			}
   380  		}
   381  		results.URLs = append(results.URLs, result)
   382  	}
   383  	return results, nil
   384  }
   385  
   386  func resolveCharm(ref *charm.URL, repo charmrepo.Interface) (*charm.URL, error) {
   387  	if ref.Schema != "cs" {
   388  		return nil, errors.New("only charm store charm references are supported, with cs: schema")
   389  	}
   390  
   391  	// Resolve the charm location with the repository.
   392  	resolved, _, err := repo.Resolve(ref)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	if resolved.Series == "" {
   397  		return nil, errors.Errorf("no series found in charm URL %q", resolved)
   398  	}
   399  	return resolved.WithRevision(ref.Revision), nil
   400  }
   401  
   402  type csStateShim struct {
   403  	*state.State
   404  }
   405  
   406  func NewStateShim(st *state.State) State {
   407  	return csStateShim{
   408  		State: st,
   409  	}
   410  }
   411  
   412  func (s csStateShim) PrepareStoreCharmUpload(curl *charm.URL) (StateCharm, error) {
   413  	charm, err := s.State.PrepareStoreCharmUpload(curl)
   414  	if err != nil {
   415  		return nil, errors.Trace(err)
   416  	}
   417  	return csStateCharmShim{Charm: charm}, nil
   418  }
   419  
   420  func (s csStateShim) Model() (StateModel, error) {
   421  	model, err := s.State.Model()
   422  	if err != nil {
   423  		return nil, errors.Trace(err)
   424  	}
   425  	return csStateModelShim{Model: model}, nil
   426  }
   427  
   428  type csStateCharmShim struct {
   429  	*state.Charm
   430  }
   431  
   432  func (s csStateCharmShim) IsUploaded() bool {
   433  	return s.Charm.IsUploaded()
   434  }
   435  
   436  type csStateModelShim struct {
   437  	*state.Model
   438  }
   439  
   440  func (s csStateModelShim) ModelConfig() (*config.Config, error) {
   441  	return s.Model.ModelConfig()
   442  }