github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/commands/common.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"path"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/persistent-cookiejar"
    15  	"github.com/juju/utils"
    16  	"golang.org/x/net/publicsuffix"
    17  	"gopkg.in/juju/charm.v5"
    18  	"gopkg.in/juju/charm.v5/charmrepo"
    19  	"gopkg.in/juju/charmstore.v4/csclient"
    20  	"gopkg.in/macaroon-bakery.v0/httpbakery"
    21  	"gopkg.in/macaroon.v1"
    22  
    23  	"github.com/juju/juju/api"
    24  	"github.com/juju/juju/apiserver/params"
    25  	"github.com/juju/juju/cmd/envcmd"
    26  	"github.com/juju/juju/environs"
    27  	"github.com/juju/juju/environs/config"
    28  	"github.com/juju/juju/environs/configstore"
    29  )
    30  
    31  // destroyPreparedEnviron destroys the environment and logs an error
    32  // if it fails.
    33  var destroyPreparedEnviron = destroyPreparedEnvironProductionFunc
    34  
    35  var logger = loggo.GetLogger("juju.cmd.juju")
    36  
    37  func destroyPreparedEnvironProductionFunc(
    38  	ctx *cmd.Context,
    39  	env environs.Environ,
    40  	store configstore.Storage,
    41  	action string,
    42  ) {
    43  	ctx.Infof("%s failed, destroying environment", action)
    44  	if err := environs.Destroy(env, store); err != nil {
    45  		logger.Errorf("the environment could not be destroyed: %v", err)
    46  	}
    47  }
    48  
    49  var destroyEnvInfo = destroyEnvInfoProductionFunc
    50  
    51  func destroyEnvInfoProductionFunc(
    52  	ctx *cmd.Context,
    53  	cfgName string,
    54  	store configstore.Storage,
    55  	action string,
    56  ) {
    57  	ctx.Infof("%s failed, cleaning up the environment.", action)
    58  	if err := environs.DestroyInfo(cfgName, store); err != nil {
    59  		logger.Errorf("the environment jenv file could not be cleaned up: %v", err)
    60  	}
    61  }
    62  
    63  // environFromName loads an existing environment or prepares a new
    64  // one. If there are no errors, it returns the environ and a closure to
    65  // clean up in case we need to further up the stack. If an error has
    66  // occurred, the environment and cleanup function will be nil, and the
    67  // error will be filled in.
    68  var environFromName = environFromNameProductionFunc
    69  
    70  func environFromNameProductionFunc(
    71  	ctx *cmd.Context,
    72  	envName string,
    73  	action string,
    74  	ensureNotBootstrapped func(environs.Environ) error,
    75  ) (env environs.Environ, cleanup func(), err error) {
    76  
    77  	store, err := configstore.Default()
    78  	if err != nil {
    79  		return nil, nil, err
    80  	}
    81  
    82  	envExisted := false
    83  	if environInfo, err := store.ReadInfo(envName); err == nil {
    84  		envExisted = true
    85  		logger.Warningf(
    86  			"ignoring environments.yaml: using bootstrap config in %s",
    87  			environInfo.Location(),
    88  		)
    89  	} else if !errors.IsNotFound(err) {
    90  		return nil, nil, err
    91  	}
    92  
    93  	cleanup = func() {
    94  		// Distinguish b/t removing the jenv file or tearing down the
    95  		// environment. We want to remove the jenv file if preparation
    96  		// was not successful. We want to tear down the environment
    97  		// only in the case where the environment didn't already
    98  		// exist.
    99  		if env == nil {
   100  			logger.Debugf("Destroying environment info.")
   101  			destroyEnvInfo(ctx, envName, store, action)
   102  		} else if !envExisted && ensureNotBootstrapped(env) != environs.ErrAlreadyBootstrapped {
   103  			logger.Debugf("Destroying environment.")
   104  			destroyPreparedEnviron(ctx, env, store, action)
   105  		}
   106  	}
   107  
   108  	if env, err = environs.PrepareFromName(envName, envcmd.BootstrapContext(ctx), store); err != nil {
   109  		return nil, cleanup, err
   110  	}
   111  
   112  	return env, cleanup, err
   113  }
   114  
   115  // resolveCharmURL resolves the given charm URL string
   116  // by looking it up in the appropriate charm repository.
   117  // If it is a charm store charm URL, the given csParams will
   118  // be used to access the charm store repository.
   119  // If it is a local charm URL, the local charm repository at
   120  // the given repoPath will be used. The given configuration
   121  // will be used to add any necessary attributes to the repo
   122  // and to resolve the default series if possible.
   123  //
   124  // resolveCharmURL also returns the charm repository holding
   125  // the charm.
   126  func resolveCharmURL(curlStr string, csParams charmrepo.NewCharmStoreParams, repoPath string, conf *config.Config) (*charm.URL, charmrepo.Interface, error) {
   127  	ref, err := charm.ParseReference(curlStr)
   128  	if err != nil {
   129  		return nil, nil, errors.Trace(err)
   130  	}
   131  	repo, err := charmrepo.InferRepository(ref, csParams, repoPath)
   132  	if err != nil {
   133  		return nil, nil, errors.Trace(err)
   134  	}
   135  	repo = config.SpecializeCharmRepo(repo, conf)
   136  	if ref.Series == "" {
   137  		if defaultSeries, ok := conf.DefaultSeries(); ok {
   138  			ref.Series = defaultSeries
   139  		}
   140  	}
   141  	if ref.Schema == "local" && ref.Series == "" {
   142  		possibleURL := *ref
   143  		possibleURL.Series = "trusty"
   144  		logger.Errorf("The series is not specified in the environment (default-series) or with the charm. Did you mean:\n\t%s", &possibleURL)
   145  		return nil, nil, errors.Errorf("cannot resolve series for charm: %q", ref)
   146  	}
   147  	if ref.Series != "" && ref.Revision != -1 {
   148  		// The URL is already fully resolved; do not
   149  		// bother with an unnecessary round-trip to the
   150  		// charm store.
   151  		curl, err := ref.URL("")
   152  		if err != nil {
   153  			panic(err)
   154  		}
   155  		return curl, repo, nil
   156  	}
   157  	curl, err := repo.Resolve(ref)
   158  	if err != nil {
   159  		return nil, nil, errors.Trace(err)
   160  	}
   161  	return curl, repo, nil
   162  }
   163  
   164  // addCharmViaAPI calls the appropriate client API calls to add the
   165  // given charm URL to state. For non-public charm URLs, this function also
   166  // handles the macaroon authorization process using the given csClient.
   167  // The resulting charm URL of the added charm is displayed on stdout.
   168  func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charmrepo.Interface, csclient *csClient) (*charm.URL, error) {
   169  	switch curl.Schema {
   170  	case "local":
   171  		ch, err := repo.Get(curl)
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  		stateCurl, err := client.AddLocalCharm(curl, ch)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		curl = stateCurl
   180  	case "cs":
   181  		if err := client.AddCharm(curl); err != nil {
   182  			if !params.IsCodeUnauthorized(err) {
   183  				return nil, errors.Mask(err)
   184  			}
   185  			m, err := csclient.authorize(curl)
   186  			if err != nil {
   187  				return nil, errors.Mask(err)
   188  			}
   189  			if err := client.AddCharmWithAuthorization(curl, m); err != nil {
   190  				return nil, errors.Mask(err)
   191  			}
   192  		}
   193  	default:
   194  		return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema)
   195  	}
   196  	ctx.Infof("Added charm %q to the environment.", curl)
   197  	return curl, nil
   198  }
   199  
   200  // csClient gives access to the charm store server and provides parameters
   201  // for connecting to the charm store.
   202  type csClient struct {
   203  	jar    *cookiejar.Jar
   204  	params charmrepo.NewCharmStoreParams
   205  }
   206  
   207  // newCharmStoreClient is called to obtain a charm store client
   208  // including the parameters for connecting to the charm store, and
   209  // helpers to save the local authorization cookies and to authorize
   210  // non-public charm deployments. It is defined as a variable so it can
   211  // be changed for testing purposes.
   212  var newCharmStoreClient = func() (*csClient, error) {
   213  	jar, client, err := newHTTPClient()
   214  	if err != nil {
   215  		return nil, errors.Mask(err)
   216  	}
   217  	return &csClient{
   218  		jar: jar,
   219  		params: charmrepo.NewCharmStoreParams{
   220  			HTTPClient:   client,
   221  			VisitWebPage: httpbakery.OpenWebBrowser,
   222  		},
   223  	}, nil
   224  }
   225  
   226  func newHTTPClient() (*cookiejar.Jar, *http.Client, error) {
   227  	cookieFile := path.Join(utils.Home(), ".go-cookies")
   228  	jar, err := cookiejar.New(&cookiejar.Options{
   229  		PublicSuffixList: publicsuffix.List,
   230  	})
   231  	if err != nil {
   232  		panic(err)
   233  	}
   234  	if err := jar.Load(cookieFile); err != nil {
   235  		return nil, nil, err
   236  	}
   237  	client := httpbakery.NewHTTPClient()
   238  	client.Jar = jar
   239  	return jar, client, nil
   240  }
   241  
   242  // authorize acquires and return the charm store delegatable macaroon to be
   243  // used to add the charm corresponding to the given URL.
   244  // The macaroon is properly attenuated so that it can only be used to deploy
   245  // the given charm URL.
   246  func (c *csClient) authorize(curl *charm.URL) (*macaroon.Macaroon, error) {
   247  	client := csclient.New(csclient.Params{
   248  		URL:          c.params.URL,
   249  		HTTPClient:   c.params.HTTPClient,
   250  		VisitWebPage: c.params.VisitWebPage,
   251  	})
   252  	var m *macaroon.Macaroon
   253  	if err := client.Get("/delegatable-macaroon", &m); err != nil {
   254  		return nil, errors.Trace(err)
   255  	}
   256  	if err := m.AddFirstPartyCaveat("is-entity " + curl.String()); err != nil {
   257  		return nil, errors.Trace(err)
   258  	}
   259  	return m, nil
   260  }