github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/resource/cmd/deploy.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cmd 5 6 import ( 7 "io" 8 "os" 9 "strings" 10 11 "github.com/juju/errors" 12 charmresource "gopkg.in/juju/charm.v6-unstable/resource" 13 "gopkg.in/macaroon.v1" 14 15 "github.com/juju/juju/charmstore" 16 ) 17 18 // DeployClient exposes the functionality of the resources API needed 19 // for deploy. 20 type DeployClient interface { 21 // AddPendingResources adds pending metadata for store-based resources. 22 AddPendingResources(applicationID string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (ids []string, err error) 23 24 // AddPendingResource uploads data and metadata for a pending resource for the given application. 25 AddPendingResource(applicationID string, resource charmresource.Resource, filename string, r io.ReadSeeker) (id string, err error) 26 } 27 28 // DeployResourcesArgs holds the arguments to DeployResources(). 29 type DeployResourcesArgs struct { 30 // ApplicationID identifies the application being deployed. 31 ApplicationID string 32 33 // CharmID identifies the application's charm. 34 CharmID charmstore.CharmID 35 36 // CharmStoreMacaroon is the macaroon to use for the charm when 37 // interacting with the charm store. 38 CharmStoreMacaroon *macaroon.Macaroon 39 40 // Filenames is the set of resources for which a filename 41 // was provided at the command-line. 42 Filenames map[string]string 43 44 // Revisions is the set of resources for which a revision 45 // was provided at the command-line. 46 Revisions map[string]int 47 48 // ResourcesMeta holds the charm metadata for each of the resources 49 // that should be added/updated on the controller. 50 ResourcesMeta map[string]charmresource.Meta 51 52 // Client is the resources API client to use during deploy. 53 Client DeployClient 54 } 55 56 // DeployResources uploads the bytes for the given files to the server and 57 // creates pending resource metadata for the all resource mentioned in the 58 // metadata. It returns a map of resource name to pending resource IDs. 59 func DeployResources(args DeployResourcesArgs) (ids map[string]string, err error) { 60 d := deployUploader{ 61 applicationID: args.ApplicationID, 62 chID: args.CharmID, 63 csMac: args.CharmStoreMacaroon, 64 client: args.Client, 65 resources: args.ResourcesMeta, 66 osOpen: func(s string) (ReadSeekCloser, error) { return os.Open(s) }, 67 osStat: func(s string) error { _, err := os.Stat(s); return err }, 68 } 69 70 ids, err = d.upload(args.Filenames, args.Revisions) 71 if err != nil { 72 return nil, errors.Trace(err) 73 } 74 return ids, nil 75 } 76 77 type deployUploader struct { 78 applicationID string 79 chID charmstore.CharmID 80 csMac *macaroon.Macaroon 81 resources map[string]charmresource.Meta 82 client DeployClient 83 osOpen func(path string) (ReadSeekCloser, error) 84 osStat func(path string) error 85 } 86 87 func (d deployUploader) upload(files map[string]string, revisions map[string]int) (map[string]string, error) { 88 if err := d.validateResources(); err != nil { 89 return nil, errors.Trace(err) 90 } 91 92 if err := d.checkExpectedResources(files, revisions); err != nil { 93 return nil, errors.Trace(err) 94 } 95 96 if err := d.checkFiles(files); err != nil { 97 return nil, errors.Trace(err) 98 } 99 100 storeResources := d.storeResources(files, revisions) 101 pending := map[string]string{} 102 if len(storeResources) > 0 { 103 ids, err := d.client.AddPendingResources(d.applicationID, d.chID, d.csMac, storeResources) 104 if err != nil { 105 return nil, errors.Trace(err) 106 } 107 // guaranteed 1:1 correlation between ids and resources. 108 for i, res := range storeResources { 109 pending[res.Name] = ids[i] 110 } 111 } 112 113 for name, filename := range files { 114 id, err := d.uploadFile(name, filename) 115 if err != nil { 116 return nil, errors.Trace(err) 117 } 118 pending[name] = id 119 } 120 121 return pending, nil 122 } 123 124 func (d deployUploader) checkFiles(files map[string]string) error { 125 for name, path := range files { 126 err := d.osStat(path) 127 if os.IsNotExist(err) { 128 return errors.Annotatef(err, "file for resource %q", name) 129 } 130 if err != nil { 131 return errors.Annotatef(err, "can't read file for resource %q", name) 132 } 133 } 134 return nil 135 } 136 137 func (d deployUploader) validateResources() error { 138 var errs []error 139 for _, meta := range d.resources { 140 if err := meta.Validate(); err != nil { 141 errs = append(errs, err) 142 } 143 } 144 if len(errs) == 1 { 145 return errors.Trace(errs[0]) 146 } 147 if len(errs) > 1 { 148 msgs := make([]string, len(errs)) 149 for i, err := range errs { 150 msgs[i] = err.Error() 151 } 152 return errors.NewNotValid(nil, strings.Join(msgs, ", ")) 153 } 154 return nil 155 } 156 157 func (d deployUploader) storeResources(uploads map[string]string, revisions map[string]int) []charmresource.Resource { 158 var resources []charmresource.Resource 159 for name, meta := range d.resources { 160 if _, ok := uploads[name]; ok { 161 continue 162 } 163 164 revision := -1 165 if rev, ok := revisions[name]; ok { 166 revision = rev 167 } 168 169 resources = append(resources, charmresource.Resource{ 170 Meta: meta, 171 Origin: charmresource.OriginStore, 172 Revision: revision, 173 // Fingerprint and Size will be added server-side in 174 // the AddPendingResources() API call. 175 }) 176 } 177 return resources 178 } 179 180 func (d deployUploader) uploadFile(resourcename, filename string) (id string, err error) { 181 f, err := d.osOpen(filename) 182 if err != nil { 183 return "", errors.Trace(err) 184 } 185 defer f.Close() 186 res := charmresource.Resource{ 187 Meta: d.resources[resourcename], 188 Origin: charmresource.OriginUpload, 189 } 190 191 id, err = d.client.AddPendingResource(d.applicationID, res, filename, f) 192 if err != nil { 193 return "", errors.Trace(err) 194 } 195 return id, err 196 } 197 198 func (d deployUploader) checkExpectedResources(filenames map[string]string, revisions map[string]int) error { 199 var unknown []string 200 for name := range filenames { 201 if _, ok := d.resources[name]; !ok { 202 unknown = append(unknown, name) 203 } 204 } 205 for name := range revisions { 206 if _, ok := d.resources[name]; !ok { 207 unknown = append(unknown, name) 208 } 209 } 210 if len(unknown) == 1 { 211 return errors.Errorf("unrecognized resource %q", unknown[0]) 212 } 213 if len(unknown) > 1 { 214 return errors.Errorf("unrecognized resources: %s", strings.Join(unknown, ", ")) 215 } 216 return nil 217 }