github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/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-unstable" 16 "gopkg.in/juju/charmrepo.v2-unstable" 17 "gopkg.in/juju/charmrepo.v2-unstable/csclient" 18 csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params" 19 "gopkg.in/macaroon-bakery.v1/httpbakery" 20 "gopkg.in/macaroon.v1" 21 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/environs/config" 24 "github.com/juju/juju/state" 25 jujuversion "github.com/juju/juju/version" 26 ) 27 28 // TODO - we really want to avoid this, which we can do by refactoring code requiring this 29 // to use interfaces. 30 // NewCharmStoreRepo instantiates a new charm store repository. 31 // It is exported for testing purposes. 32 var NewCharmStoreRepo = newCharmStoreFromClient 33 34 func newCharmStoreFromClient(csClient *csclient.Client) charmrepo.Interface { 35 return charmrepo.NewCharmStoreFromClient(csClient) 36 } 37 38 // AddCharmWithAuthorization adds the given charm URL (which must include revision) to 39 // the environment, if it does not exist yet. Local charms are not 40 // supported, only charm store URLs. See also AddLocalCharm(). 41 // 42 // The authorization macaroon, args.CharmStoreMacaroon, may be 43 // omitted, in which case this call is equivalent to AddCharm. 44 func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error { 45 charmURL, err := charm.ParseURL(args.URL) 46 if err != nil { 47 return err 48 } 49 if charmURL.Schema != "cs" { 50 return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") 51 } 52 if charmURL.Revision < 0 { 53 return fmt.Errorf("charm URL must include revision") 54 } 55 56 // First, check if a pending or a real charm exists in state. 57 stateCharm, err := st.PrepareStoreCharmUpload(charmURL) 58 if err != nil { 59 return err 60 } 61 if stateCharm.IsUploaded() { 62 // Charm already in state (it was uploaded already). 63 return nil 64 } 65 66 // Open a charm store client. 67 repo, err := openCSRepo(args) 68 if err != nil { 69 return err 70 } 71 modelConfig, err := st.ModelConfig() 72 if err != nil { 73 return err 74 } 75 repo = config.SpecializeCharmRepo(repo, modelConfig).(*charmrepo.CharmStore) 76 77 // Get the charm and its information from the store. 78 downloadedCharm, err := repo.Get(charmURL) 79 if err != nil { 80 cause := errors.Cause(err) 81 if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { 82 return errors.NewUnauthorized(err, "") 83 } 84 return errors.Trace(err) 85 } 86 87 if err := checkMinVersion(downloadedCharm); err != nil { 88 return errors.Trace(err) 89 } 90 91 // Open it and calculate the SHA256 hash. 92 downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) 93 if !ok { 94 return errors.Errorf("expected a charm archive, got %T", downloadedCharm) 95 } 96 archive, err := os.Open(downloadedBundle.Path) 97 if err != nil { 98 return errors.Annotate(err, "cannot read downloaded charm") 99 } 100 defer archive.Close() 101 bundleSHA256, size, err := utils.ReadSHA256(archive) 102 if err != nil { 103 return errors.Annotate(err, "cannot calculate SHA256 hash of charm") 104 } 105 if _, err := archive.Seek(0, 0); err != nil { 106 return errors.Annotate(err, "cannot rewind charm archive") 107 } 108 109 ca := CharmArchive{ 110 ID: charmURL, 111 Charm: downloadedCharm, 112 Data: archive, 113 Size: size, 114 SHA256: bundleSHA256, 115 } 116 if args.CharmStoreMacaroon != nil { 117 ca.Macaroon = macaroon.Slice{args.CharmStoreMacaroon} 118 } 119 120 // Store the charm archive in environment storage. 121 return StoreCharmArchive(st, ca) 122 } 123 124 func openCSRepo(args params.AddCharmWithAuthorization) (charmrepo.Interface, error) { 125 csClient, err := openCSClient(args) 126 if err != nil { 127 return nil, err 128 } 129 repo := NewCharmStoreRepo(csClient) 130 return repo, nil 131 } 132 133 func openCSClient(args params.AddCharmWithAuthorization) (*csclient.Client, error) { 134 csURL, err := url.Parse(csclient.ServerURL) 135 if err != nil { 136 return nil, err 137 } 138 csParams := csclient.Params{ 139 URL: csURL.String(), 140 HTTPClient: httpbakery.NewHTTPClient(), 141 } 142 143 if args.CharmStoreMacaroon != nil { 144 // Set the provided charmstore authorizing macaroon 145 // as a cookie in the HTTP client. 146 // TODO(cmars) discharge any third party caveats in the macaroon. 147 ms := []*macaroon.Macaroon{args.CharmStoreMacaroon} 148 httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms) 149 } 150 csClient := csclient.New(csParams) 151 channel := csparams.Channel(args.Channel) 152 if channel != csparams.NoChannel { 153 csClient = csClient.WithChannel(channel) 154 } 155 return csClient, nil 156 } 157 158 func checkMinVersion(ch charm.Charm) error { 159 minver := ch.Meta().MinJujuVersion 160 if minver != version.Zero && minver.Compare(jujuversion.Current) > 0 { 161 return minVersionError(minver, jujuversion.Current) 162 } 163 return nil 164 } 165 166 type minJujuVersionErr struct { 167 *errors.Err 168 } 169 170 func minVersionError(minver, jujuver version.Number) error { 171 err := errors.NewErr("charm's min version (%s) is higher than this juju environment's version (%s)", 172 minver, jujuver) 173 err.SetLocation(1) 174 return minJujuVersionErr{&err} 175 } 176 177 // CharmArchive is the data that needs to be stored for a charm archive in 178 // state. 179 type CharmArchive struct { 180 // ID is the charm URL for which we're storing the archive. 181 ID *charm.URL 182 183 // Charm is the metadata about the charm for the archive. 184 Charm charm.Charm 185 186 // Data contains the bytes of the archive. 187 Data io.Reader 188 189 // Size is the number of bytes in Data. 190 Size int64 191 192 // SHA256 is the hash of the bytes in Data. 193 SHA256 string 194 195 // Macaroon is the authorization macaroon for accessing the charmstore. 196 Macaroon macaroon.Slice 197 } 198 199 // StoreCharmArchive stores a charm archive in environment storage. 200 func StoreCharmArchive(st *state.State, archive CharmArchive) error { 201 storage := newStateStorage(st.ModelUUID(), st.MongoSession()) 202 storagePath, err := charmArchiveStoragePath(archive.ID) 203 if err != nil { 204 return errors.Annotate(err, "cannot generate charm archive name") 205 } 206 if err := storage.Put(storagePath, archive.Data, archive.Size); err != nil { 207 return errors.Annotate(err, "cannot add charm to storage") 208 } 209 210 info := state.CharmInfo{ 211 Charm: archive.Charm, 212 ID: archive.ID, 213 StoragePath: storagePath, 214 SHA256: archive.SHA256, 215 Macaroon: archive.Macaroon, 216 } 217 218 // Now update the charm data in state and mark it as no longer pending. 219 _, err = st.UpdateUploadedCharm(info) 220 if err != nil { 221 alreadyUploaded := err == state.ErrCharmRevisionAlreadyModified || 222 errors.Cause(err) == state.ErrCharmRevisionAlreadyModified || 223 state.IsCharmAlreadyUploadedError(err) 224 if err := storage.Remove(storagePath); err != nil { 225 if alreadyUploaded { 226 logger.Errorf("cannot remove duplicated charm archive from storage: %v", err) 227 } else { 228 logger.Errorf("cannot remove unsuccessfully recorded charm archive from storage: %v", err) 229 } 230 } 231 if alreadyUploaded { 232 // Somebody else managed to upload and update the charm in 233 // state before us. This is not an error. 234 return nil 235 } 236 } 237 return nil 238 } 239 240 // charmArchiveStoragePath returns a string that is suitable as a 241 // storage path, using a random UUID to avoid colliding with concurrent 242 // uploads. 243 func charmArchiveStoragePath(curl *charm.URL) (string, error) { 244 uuid, err := utils.NewUUID() 245 if err != nil { 246 return "", err 247 } 248 return fmt.Sprintf("charms/%s-%s", curl.String(), uuid), nil 249 } 250 251 // ResolveCharm resolves the best available charm URLs with series, for charm 252 // locations without a series specified. 253 func ResolveCharms(st *state.State, args params.ResolveCharms) (params.ResolveCharmResults, error) { 254 var results params.ResolveCharmResults 255 256 envConfig, err := st.ModelConfig() 257 if err != nil { 258 return params.ResolveCharmResults{}, err 259 } 260 repo := config.SpecializeCharmRepo( 261 NewCharmStoreRepo(csclient.New(csclient.Params{})), 262 envConfig) 263 264 for _, ref := range args.References { 265 result := params.ResolveCharmResult{} 266 curl, err := charm.ParseURL(ref) 267 if err != nil { 268 result.Error = err.Error() 269 } else { 270 curl, err := resolveCharm(curl, repo) 271 if err != nil { 272 result.Error = err.Error() 273 } else { 274 result.URL = curl.String() 275 } 276 } 277 results.URLs = append(results.URLs, result) 278 } 279 return results, nil 280 } 281 282 func resolveCharm(ref *charm.URL, repo charmrepo.Interface) (*charm.URL, error) { 283 if ref.Schema != "cs" { 284 return nil, fmt.Errorf("only charm store charm references are supported, with cs: schema") 285 } 286 287 // Resolve the charm location with the repository. 288 resolved, _, err := repo.Resolve(ref) 289 if err != nil { 290 return nil, err 291 } 292 if resolved.Series == "" { 293 return nil, errors.Errorf("no series found in charm URL %q", resolved) 294 } 295 return resolved.WithRevision(ref.Revision), nil 296 }