github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/modelcmd/base.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package modelcmd 5 6 import ( 7 "net/http" 8 9 "github.com/juju/cmd" 10 "github.com/juju/errors" 11 "gopkg.in/macaroon-bakery.v1/httpbakery" 12 "launchpad.net/gnuflag" 13 14 "github.com/juju/juju/api" 15 "github.com/juju/juju/api/base" 16 "github.com/juju/juju/api/modelmanager" 17 "github.com/juju/juju/cloud" 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/config" 20 "github.com/juju/juju/juju" 21 "github.com/juju/juju/jujuclient" 22 ) 23 24 var errNoNameSpecified = errors.New("no name specified") 25 26 // CommandBase extends cmd.Command with a closeContext method. 27 // It is implicitly implemented by any type that embeds JujuCommandBase. 28 type CommandBase interface { 29 cmd.Command 30 31 // closeContext closes the command's API context. 32 closeContext() 33 setCmdContext(*cmd.Context) 34 } 35 36 // ModelAPI provides access to the model client facade methods. 37 type ModelAPI interface { 38 ListModels(user string) ([]base.UserModel, error) 39 Close() error 40 } 41 42 // JujuCommandBase is a convenience type for embedding that need 43 // an API connection. 44 type JujuCommandBase struct { 45 cmd.CommandBase 46 cmdContext *cmd.Context 47 apiContext *APIContext 48 modelApi ModelAPI 49 } 50 51 // closeContext closes the command's API context 52 // if it has actually been created. 53 func (c *JujuCommandBase) closeContext() { 54 if c.apiContext != nil { 55 if err := c.apiContext.Close(); err != nil { 56 logger.Errorf("%v", err) 57 } 58 } 59 } 60 61 // SetModelApi sets the api used to access model information. 62 func (c *JujuCommandBase) SetModelApi(api ModelAPI) { 63 c.modelApi = api 64 } 65 66 func (c *JujuCommandBase) modelAPI(store jujuclient.ClientStore, controllerName, accountName string) (ModelAPI, error) { 67 if c.modelApi != nil { 68 return c.modelApi, nil 69 } 70 conn, err := c.NewAPIRoot(store, controllerName, accountName, "") 71 if err != nil { 72 return nil, errors.Trace(err) 73 } 74 c.modelApi = modelmanager.NewClient(conn) 75 return c.modelApi, nil 76 } 77 78 // NewAPIRoot returns a new connection to the API server for the given 79 // model or controller. 80 func (c *JujuCommandBase) NewAPIRoot( 81 store jujuclient.ClientStore, 82 controllerName, accountName, modelName string, 83 ) (api.Connection, error) { 84 params, err := c.NewAPIConnectionParams( 85 store, controllerName, accountName, modelName, 86 ) 87 if err != nil { 88 return nil, errors.Trace(err) 89 } 90 return juju.NewAPIConnection(params) 91 } 92 93 // NewAPIConnectionParams returns a juju.NewAPIConnectionParams with the 94 // given arguments such that a call to juju.NewAPIConnection with the 95 // result behaves the same as a call to JujuCommandBase.NewAPIRoot with 96 // the same arguments. 97 func (c *JujuCommandBase) NewAPIConnectionParams( 98 store jujuclient.ClientStore, 99 controllerName, accountName, modelName string, 100 ) (juju.NewAPIConnectionParams, error) { 101 if err := c.initAPIContext(); err != nil { 102 return juju.NewAPIConnectionParams{}, errors.Trace(err) 103 } 104 return newAPIConnectionParams(store, controllerName, accountName, modelName, c.apiContext.BakeryClient) 105 } 106 107 // HTTPClient returns an http.Client that contains the loaded 108 // persistent cookie jar. Note that this client is not good for 109 // connecting to the Juju API itself because it does not 110 // have the correct TLS setup - use api.Connection.HTTPClient 111 // for that. 112 func (c *JujuCommandBase) HTTPClient() (*http.Client, error) { 113 if err := c.initAPIContext(); err != nil { 114 return nil, errors.Trace(err) 115 } 116 return c.apiContext.BakeryClient.Client, nil 117 } 118 119 // BakeryClient returns a macaroon bakery client that 120 // uses the same HTTP client returned by HTTPClient. 121 func (c *JujuCommandBase) BakeryClient() (*httpbakery.Client, error) { 122 if err := c.initAPIContext(); err != nil { 123 return nil, errors.Trace(err) 124 } 125 return c.apiContext.BakeryClient, nil 126 } 127 128 // APIOpen establishes a connection to the API server using the 129 // the given api.Info and api.DialOpts. 130 func (c *JujuCommandBase) APIOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 131 if err := c.initAPIContext(); err != nil { 132 return nil, errors.Trace(err) 133 } 134 return c.apiOpen(info, opts) 135 } 136 137 // RefreshModels refreshes the local models cache for the current user 138 // on the specified controller. 139 func (c *JujuCommandBase) RefreshModels(store jujuclient.ClientStore, controllerName, accountName string) error { 140 accountDetails, err := store.AccountByName(controllerName, accountName) 141 if err != nil { 142 return errors.Trace(err) 143 } 144 145 modelManager, err := c.modelAPI(store, controllerName, accountName) 146 if err != nil { 147 return errors.Trace(err) 148 } 149 defer modelManager.Close() 150 151 models, err := modelManager.ListModels(accountDetails.User) 152 if err != nil { 153 return errors.Trace(err) 154 } 155 for _, model := range models { 156 modelDetails := jujuclient.ModelDetails{model.UUID} 157 if err := store.UpdateModel(controllerName, accountName, model.Name, modelDetails); err != nil { 158 return errors.Trace(err) 159 } 160 } 161 return nil 162 } 163 164 // initAPIContext lazily initializes c.apiContext. Doing this lazily means that 165 // we avoid unnecessarily loading and saving the cookies 166 // when a command does not actually make an API connection. 167 func (c *JujuCommandBase) initAPIContext() error { 168 if c.apiContext != nil { 169 return nil 170 } 171 apiContext, err := NewAPIContext(c.cmdContext) 172 if err != nil { 173 return errors.Trace(err) 174 } 175 c.apiContext = apiContext 176 return nil 177 } 178 179 func (c *JujuCommandBase) setCmdContext(ctx *cmd.Context) { 180 c.cmdContext = ctx 181 } 182 183 // apiOpen establishes a connection to the API server using the 184 // the give api.Info and api.DialOpts. 185 func (c *JujuCommandBase) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 186 return api.Open(info, opts) 187 } 188 189 // WrapBase wraps the specified CommandBase, returning a Command 190 // that proxies to each of the CommandBase methods. 191 func WrapBase(c CommandBase) cmd.Command { 192 return &baseCommandWrapper{ 193 CommandBase: c, 194 } 195 } 196 197 type baseCommandWrapper struct { 198 CommandBase 199 } 200 201 // Run implements Command.Run. 202 func (w *baseCommandWrapper) Run(ctx *cmd.Context) error { 203 defer w.closeContext() 204 w.setCmdContext(ctx) 205 return w.CommandBase.Run(ctx) 206 } 207 208 // SetFlags implements Command.SetFlags. 209 func (w *baseCommandWrapper) SetFlags(f *gnuflag.FlagSet) { 210 w.CommandBase.SetFlags(f) 211 } 212 213 // Init implements Command.Init. 214 func (w *baseCommandWrapper) Init(args []string) error { 215 return w.CommandBase.Init(args) 216 } 217 218 func newAPIConnectionParams( 219 store jujuclient.ClientStore, 220 controllerName, 221 accountName, 222 modelName string, 223 bakery *httpbakery.Client, 224 ) (juju.NewAPIConnectionParams, error) { 225 if controllerName == "" { 226 return juju.NewAPIConnectionParams{}, errors.Trace(errNoNameSpecified) 227 } 228 var accountDetails *jujuclient.AccountDetails 229 if accountName != "" { 230 var err error 231 accountDetails, err = store.AccountByName(controllerName, accountName) 232 if err != nil { 233 return juju.NewAPIConnectionParams{}, errors.Trace(err) 234 } 235 } 236 var modelUUID string 237 if modelName != "" { 238 modelDetails, err := store.ModelByName(controllerName, accountName, modelName) 239 if err != nil { 240 return juju.NewAPIConnectionParams{}, errors.Trace(err) 241 } 242 modelUUID = modelDetails.ModelUUID 243 } 244 dialOpts := api.DefaultDialOpts() 245 dialOpts.BakeryClient = bakery 246 return juju.NewAPIConnectionParams{ 247 Store: store, 248 ControllerName: controllerName, 249 BootstrapConfig: NewGetBootstrapConfigFunc(store), 250 AccountDetails: accountDetails, 251 ModelUUID: modelUUID, 252 DialOpts: dialOpts, 253 }, nil 254 } 255 256 // NewGetBootstrapConfigFunc returns a function that, given a controller name, 257 // returns the bootstrap config for that controller in the given client store. 258 func NewGetBootstrapConfigFunc(store jujuclient.ClientStore) func(string) (*config.Config, error) { 259 return bootstrapConfigGetter{store}.getBootstrapConfig 260 } 261 262 type bootstrapConfigGetter struct { 263 jujuclient.ClientStore 264 } 265 266 func (g bootstrapConfigGetter) getBootstrapConfig(controllerName string) (*config.Config, error) { 267 controllerName, err := ResolveControllerName(g.ClientStore, controllerName) 268 if err != nil { 269 return nil, errors.Annotate(err, "resolving controller name") 270 } 271 bootstrapConfig, err := g.BootstrapConfigForController(controllerName) 272 if err != nil { 273 return nil, errors.Annotate(err, "getting bootstrap config") 274 } 275 cloudType, ok := bootstrapConfig.Config["type"].(string) 276 if !ok { 277 return nil, errors.NotFoundf("cloud type in bootstrap config") 278 } 279 280 var credential *cloud.Credential 281 if bootstrapConfig.Credential != "" { 282 credential, _, _, err = GetCredentials( 283 g.ClientStore, 284 bootstrapConfig.CloudRegion, 285 bootstrapConfig.Credential, 286 bootstrapConfig.Cloud, 287 cloudType, 288 ) 289 if err != nil { 290 return nil, errors.Trace(err) 291 } 292 } else { 293 // The credential was auto-detected; run auto-detection again. 294 cloudCredential, err := DetectCredential( 295 bootstrapConfig.Cloud, cloudType, 296 ) 297 if err != nil { 298 return nil, errors.Trace(err) 299 } 300 // DetectCredential ensures that there is only one credential 301 // to choose from. It's still in a map, though, hence for..range. 302 for _, one := range cloudCredential.AuthCredentials { 303 credential = &one 304 } 305 } 306 307 // Add attributes from the controller details. 308 controllerDetails, err := g.ControllerByName(controllerName) 309 if err != nil { 310 return nil, errors.Trace(err) 311 } 312 bootstrapConfig.Config[config.CACertKey] = controllerDetails.CACert 313 bootstrapConfig.Config[config.UUIDKey] = controllerDetails.ControllerUUID 314 bootstrapConfig.Config[config.ControllerUUIDKey] = controllerDetails.ControllerUUID 315 316 cfg, err := config.New(config.UseDefaults, bootstrapConfig.Config) 317 if err != nil { 318 return nil, errors.Trace(err) 319 } 320 provider, err := environs.Provider(cfg.Type()) 321 if err != nil { 322 return nil, errors.Trace(err) 323 } 324 return provider.BootstrapConfig(environs.BootstrapConfigParams{ 325 cfg, *credential, 326 bootstrapConfig.CloudRegion, 327 bootstrapConfig.CloudEndpoint, 328 bootstrapConfig.CloudStorageEndpoint, 329 }) 330 }