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