github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/application/charmstore.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application 5 6 import ( 7 "fmt" 8 "io" 9 "net/url" 10 "os" 11 12 "github.com/juju/errors" 13 "github.com/juju/utils" 14 "github.com/juju/version" 15 "gopkg.in/juju/charm.v6" 16 "gopkg.in/juju/charmrepo.v3" 17 "gopkg.in/juju/charmrepo.v3/csclient" 18 csparams "gopkg.in/juju/charmrepo.v3/csclient/params" 19 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 20 "gopkg.in/macaroon.v2-unstable" 21 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/controller" 24 "github.com/juju/juju/core/lxdprofile" 25 "github.com/juju/juju/environs/config" 26 "github.com/juju/juju/state" 27 "github.com/juju/juju/state/storage" 28 jujuversion "github.com/juju/juju/version" 29 ) 30 31 //go:generate mockgen -package mocks -destination mocks/storage_mock.go github.com/juju/juju/state/storage Storage 32 //go:generate mockgen -package mocks -destination mocks/interface_mock.go gopkg.in/juju/charmrepo.v3 Interface 33 //go:generate mockgen -package mocks -destination mocks/charm_mock.go github.com/juju/juju/apiserver/facades/client/application StateCharm 34 //go:generate mockgen -package mocks -destination mocks/model_mock.go github.com/juju/juju/apiserver/facades/client/application StateModel 35 //go:generate mockgen -package mocks -destination mocks/charmstore_mock.go github.com/juju/juju/apiserver/facades/client/application State 36 37 // TODO - we really want to avoid this, which we can do by refactoring code requiring this 38 // to use interfaces. 39 40 // NewCharmStoreRepo instantiates a new charm store repository. 41 // It is exported for testing purposes. 42 var NewCharmStoreRepo = newCharmStoreFromClient 43 44 var newStateStorage = storage.NewStorage 45 46 func newCharmStoreFromClient(csClient *csclient.Client) charmrepo.Interface { 47 return charmrepo.NewCharmStoreFromClient(csClient) 48 } 49 50 // StateCharm represents a Charm from the state package 51 type StateCharm interface { 52 IsUploaded() bool 53 } 54 55 // StateModel represents a Model from the state package 56 type StateModel interface { 57 ModelConfig() (*config.Config, error) 58 } 59 60 // CharmState represents directives for accessing charm methods 61 type CharmState interface { 62 UpdateUploadedCharm(info state.CharmInfo) (*state.Charm, error) 63 PrepareStoreCharmUpload(curl *charm.URL) (StateCharm, error) 64 } 65 66 // ModelState represents methods for accessing model definitions 67 type ModelState interface { 68 Model() (StateModel, error) 69 ModelUUID() string 70 } 71 72 // ControllerState represents information defined for accessing controller 73 // configuration 74 type ControllerState interface { 75 ControllerConfig() (controller.Config, error) 76 } 77 78 // State represents the access patterns for the charm store methods. 79 type State interface { 80 CharmState 81 ModelState 82 ControllerState 83 state.MongoSessioner 84 } 85 86 // AddCharmWithAuthorizationAndRepo adds the given charm URL (which must include 87 // revision) to the environment, if it does not exist yet. 88 // Local charms are not supported, only charm store URLs. 89 // See also AddLocalCharm(). 90 // Additionally a Repo (See charmrepo.Interface) function factory can be 91 // provided to help with overriding the source of downloading charms. The main 92 // benefit of this indirection is to help with testing (mocking) 93 // 94 // The authorization macaroon, args.CharmStoreMacaroon, may be 95 // omitted, in which case this call is equivalent to AddCharm. 96 func AddCharmWithAuthorizationAndRepo(st State, args params.AddCharmWithAuthorization, repoFn func() (charmrepo.Interface, error)) error { 97 charmURL, err := charm.ParseURL(args.URL) 98 if err != nil { 99 return err 100 } 101 if charmURL.Schema != "cs" { 102 return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") 103 } 104 if charmURL.Revision < 0 { 105 return fmt.Errorf("charm URL must include revision") 106 } 107 108 // First, check if a pending or a real charm exists in state. 109 stateCharm, err := st.PrepareStoreCharmUpload(charmURL) 110 if err != nil { 111 return err 112 } 113 if stateCharm.IsUploaded() { 114 // Charm already in state (it was uploaded already). 115 return nil 116 } 117 118 // Get the repo from the constructor 119 repo, err := repoFn() 120 121 // Get the charm and its information from the store. 122 downloadedCharm, err := repo.Get(charmURL) 123 if err != nil { 124 cause := errors.Cause(err) 125 if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { 126 return errors.NewUnauthorized(err, "") 127 } 128 return errors.Trace(err) 129 } 130 131 if err := checkMinVersion(downloadedCharm); err != nil { 132 return errors.Trace(err) 133 } 134 135 // Open it and calculate the SHA256 hash. 136 downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) 137 if !ok { 138 return errors.Errorf("expected a charm archive, got %T", downloadedCharm) 139 } 140 141 // Validate the charm lxd profile once we've downloaded it. 142 if err := lxdprofile.ValidateCharmLXDProfile(downloadedCharm); err != nil { 143 if !args.Force { 144 return errors.Annotate(err, "cannot add charm") 145 } 146 } 147 148 // Clean up the downloaded charm - we don't need to cache it in 149 // the filesystem as well as in blob storage. 150 defer os.Remove(downloadedBundle.Path) 151 152 archive, err := os.Open(downloadedBundle.Path) 153 if err != nil { 154 return errors.Annotate(err, "cannot read downloaded charm") 155 } 156 defer archive.Close() 157 bundleSHA256, size, err := utils.ReadSHA256(archive) 158 if err != nil { 159 return errors.Annotate(err, "cannot calculate SHA256 hash of charm") 160 } 161 if _, err := archive.Seek(0, 0); err != nil { 162 return errors.Annotate(err, "cannot rewind charm archive") 163 } 164 165 ca := CharmArchive{ 166 ID: charmURL, 167 Charm: downloadedCharm, 168 Data: archive, 169 Size: size, 170 SHA256: bundleSHA256, 171 CharmVersion: downloadedBundle.Version(), 172 } 173 if args.CharmStoreMacaroon != nil { 174 ca.Macaroon = macaroon.Slice{args.CharmStoreMacaroon} 175 } 176 177 // Store the charm archive in environment storage. 178 return StoreCharmArchive(st, ca) 179 } 180 181 // AddCharmWithAuthorization adds the given charm URL (which must include revision) to 182 // the environment, if it does not exist yet. Local charms are not 183 // supported, only charm store URLs. See also AddLocalCharm(). 184 // 185 // The authorization macaroon, args.CharmStoreMacaroon, may be 186 // omitted, in which case this call is equivalent to AddCharm. 187 func AddCharmWithAuthorization(st State, args params.AddCharmWithAuthorization) error { 188 return AddCharmWithAuthorizationAndRepo(st, args, func() (charmrepo.Interface, error) { 189 // determine which charmstore api url to use. 190 controllerCfg, err := st.ControllerConfig() 191 if err != nil { 192 return nil, err 193 } 194 195 repo, err := openCSRepo(controllerCfg.CharmStoreURL(), args) 196 if err != nil { 197 return nil, err 198 } 199 model, err := st.Model() 200 if err != nil { 201 return nil, errors.Trace(err) 202 } 203 modelConfig, err := model.ModelConfig() 204 if err != nil { 205 return nil, errors.Trace(err) 206 } 207 repo = config.SpecializeCharmRepo(repo, modelConfig).(*charmrepo.CharmStore) 208 return repo, nil 209 }) 210 } 211 212 func openCSRepo(csURL string, args params.AddCharmWithAuthorization) (charmrepo.Interface, error) { 213 csClient, err := openCSClient(csURL, args) 214 if err != nil { 215 return nil, err 216 } 217 repo := NewCharmStoreRepo(csClient) 218 return repo, nil 219 } 220 221 func openCSClient(csAPIURL string, args params.AddCharmWithAuthorization) (*csclient.Client, error) { 222 csURL, err := url.Parse(csAPIURL) 223 if err != nil { 224 return nil, err 225 } 226 csParams := csclient.Params{ 227 URL: csURL.String(), 228 HTTPClient: httpbakery.NewHTTPClient(), 229 } 230 231 if args.CharmStoreMacaroon != nil { 232 // Set the provided charmstore authorizing macaroon 233 // as a cookie in the HTTP client. 234 // TODO(cmars) discharge any third party caveats in the macaroon. 235 ms := []*macaroon.Macaroon{args.CharmStoreMacaroon} 236 httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms) 237 } 238 csClient := csclient.New(csParams) 239 channel := csparams.Channel(args.Channel) 240 if channel != csparams.NoChannel { 241 csClient = csClient.WithChannel(channel) 242 } 243 return csClient, nil 244 } 245 246 func checkMinVersion(ch charm.Charm) error { 247 minver := ch.Meta().MinJujuVersion 248 if minver != version.Zero && minver.Compare(jujuversion.Current) > 0 { 249 return minVersionError(minver, jujuversion.Current) 250 } 251 return nil 252 } 253 254 type minJujuVersionErr struct { 255 *errors.Err 256 } 257 258 func minVersionError(minver, jujuver version.Number) error { 259 err := errors.NewErr("charm's min version (%s) is higher than this juju model's version (%s)", 260 minver, jujuver) 261 err.SetLocation(1) 262 return minJujuVersionErr{&err} 263 } 264 265 // CharmArchive is the data that needs to be stored for a charm archive in 266 // state. 267 type CharmArchive struct { 268 // ID is the charm URL for which we're storing the archive. 269 ID *charm.URL 270 271 // Charm is the metadata about the charm for the archive. 272 Charm charm.Charm 273 274 // Data contains the bytes of the archive. 275 Data io.Reader 276 277 // Size is the number of bytes in Data. 278 Size int64 279 280 // SHA256 is the hash of the bytes in Data. 281 SHA256 string 282 283 // Macaroon is the authorization macaroon for accessing the charmstore. 284 Macaroon macaroon.Slice 285 286 // Charm Version contains semantic version of charm, typically the output of git describe. 287 CharmVersion string 288 } 289 290 // StoreCharmArchive stores a charm archive in environment storage. 291 func StoreCharmArchive(st State, archive CharmArchive) error { 292 storage := newStateStorage(st.ModelUUID(), st.MongoSession()) 293 storagePath, err := charmArchiveStoragePath(archive.ID) 294 if err != nil { 295 return errors.Annotate(err, "cannot generate charm archive name") 296 } 297 if err := storage.Put(storagePath, archive.Data, archive.Size); err != nil { 298 return errors.Annotate(err, "cannot add charm to storage") 299 } 300 301 info := state.CharmInfo{ 302 Charm: archive.Charm, 303 ID: archive.ID, 304 StoragePath: storagePath, 305 SHA256: archive.SHA256, 306 Macaroon: archive.Macaroon, 307 Version: archive.CharmVersion, 308 } 309 310 // Now update the charm data in state and mark it as no longer pending. 311 _, err = st.UpdateUploadedCharm(info) 312 if err != nil { 313 alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified || 314 errors.Cause(err) == state.ErrCharmRevisionAlreadyModified || 315 state.IsCharmAlreadyUploadedError(err) 316 if err := storage.Remove(storagePath); err != nil { 317 if alreadyUploaded { 318 logger.Errorf("cannot remove duplicated charm archive from storage: %v", err) 319 } else { 320 logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err) 321 } 322 } 323 if alreadyUploaded { 324 // Somebody else managed to upload and update the charm in 325 // state before us. This is not an error. 326 return nil 327 } 328 return errors.Trace(err) 329 } 330 return nil 331 } 332 333 // charmArchiveStoragePath returns a string that is suitable as a 334 // storage path, using a random UUID to avoid colliding with concurrent 335 // uploads. 336 func charmArchiveStoragePath(curl *charm.URL) (string, error) { 337 uuid, err := utils.NewUUID() 338 if err != nil { 339 return "", err 340 } 341 return fmt.Sprintf("charms/%s-%s", curl.String(), uuid), nil 342 } 343 344 // ResolveCharm resolves the best available charm URLs with series, for charm 345 // locations without a series specified. 346 func ResolveCharms(st State, args params.ResolveCharms) (params.ResolveCharmResults, error) { 347 var results params.ResolveCharmResults 348 349 model, err := st.Model() 350 if err != nil { 351 return params.ResolveCharmResults{}, errors.Trace(err) 352 } 353 envConfig, err := model.ModelConfig() 354 if err != nil { 355 return params.ResolveCharmResults{}, err 356 } 357 controllerCfg, err := st.ControllerConfig() 358 if err != nil { 359 return params.ResolveCharmResults{}, err 360 } 361 csParams := csclient.Params{ 362 URL: controllerCfg.CharmStoreURL(), 363 } 364 repo := config.SpecializeCharmRepo( 365 NewCharmStoreRepo(csclient.New(csParams)), 366 envConfig) 367 368 for _, ref := range args.References { 369 result := params.ResolveCharmResult{} 370 curl, err := charm.ParseURL(ref) 371 if err != nil { 372 result.Error = err.Error() 373 } else { 374 curl, err := resolveCharm(curl, repo) 375 if err != nil { 376 result.Error = err.Error() 377 } else { 378 result.URL = curl.String() 379 } 380 } 381 results.URLs = append(results.URLs, result) 382 } 383 return results, nil 384 } 385 386 func resolveCharm(ref *charm.URL, repo charmrepo.Interface) (*charm.URL, error) { 387 if ref.Schema != "cs" { 388 return nil, errors.New("only charm store charm references are supported, with cs: schema") 389 } 390 391 // Resolve the charm location with the repository. 392 resolved, _, err := repo.Resolve(ref) 393 if err != nil { 394 return nil, err 395 } 396 if resolved.Series == "" { 397 return nil, errors.Errorf("no series found in charm URL %q", resolved) 398 } 399 return resolved.WithRevision(ref.Revision), nil 400 } 401 402 type csStateShim struct { 403 *state.State 404 } 405 406 func NewStateShim(st *state.State) State { 407 return csStateShim{ 408 State: st, 409 } 410 } 411 412 func (s csStateShim) PrepareStoreCharmUpload(curl *charm.URL) (StateCharm, error) { 413 charm, err := s.State.PrepareStoreCharmUpload(curl) 414 if err != nil { 415 return nil, errors.Trace(err) 416 } 417 return csStateCharmShim{Charm: charm}, nil 418 } 419 420 func (s csStateShim) Model() (StateModel, error) { 421 model, err := s.State.Model() 422 if err != nil { 423 return nil, errors.Trace(err) 424 } 425 return csStateModelShim{Model: model}, nil 426 } 427 428 type csStateCharmShim struct { 429 *state.Charm 430 } 431 432 func (s csStateCharmShim) IsUploaded() bool { 433 return s.Charm.IsUploaded() 434 } 435 436 type csStateModelShim struct { 437 *state.Model 438 } 439 440 func (s csStateModelShim) ModelConfig() (*config.Config, error) { 441 return s.Model.ModelConfig() 442 }