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 }