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 }