github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/resource/deploy.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package resource 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "strings" 14 15 "github.com/juju/errors" 16 charmresource "gopkg.in/juju/charm.v6/resource" 17 "gopkg.in/macaroon.v2-unstable" 18 "gopkg.in/yaml.v2" 19 20 "github.com/juju/juju/charmstore" 21 resources "github.com/juju/juju/core/resources" 22 ) 23 24 // DeployClient exposes the functionality of the resources API needed 25 // for deploy. 26 type DeployClient interface { 27 // AddPendingResources adds pending metadata for store-based resources. 28 AddPendingResources(applicationID string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (ids []string, err error) 29 30 // UploadPendingResource uploads data and metadata for a pending resource for the given application. 31 UploadPendingResource(applicationID string, resource charmresource.Resource, filename string, r io.ReadSeeker) (id string, err error) 32 } 33 34 // DeployResourcesArgs holds the arguments to DeployResources(). 35 type DeployResourcesArgs struct { 36 // ApplicationID identifies the application being deployed. 37 ApplicationID string 38 39 // CharmID identifies the application's charm. 40 CharmID charmstore.CharmID 41 42 // CharmStoreMacaroon is the macaroon to use for the charm when 43 // interacting with the charm store. 44 CharmStoreMacaroon *macaroon.Macaroon 45 46 // ResourceValues is the set of resources for which a value 47 // was provided at the command-line. 48 ResourceValues map[string]string 49 50 // Revisions is the set of resources for which a revision 51 // was provided at the command-line. 52 Revisions map[string]int 53 54 // ResourcesMeta holds the charm metadata for each of the resources 55 // that should be added/updated on the controller. 56 ResourcesMeta map[string]charmresource.Meta 57 58 // Client is the resources API client to use during deploy. 59 Client DeployClient 60 } 61 62 // DeployResources uploads the bytes for the given files to the server and 63 // creates pending resource metadata for the all resource mentioned in the 64 // metadata. It returns a map of resource name to pending resource IDs. 65 func DeployResources(args DeployResourcesArgs) (ids map[string]string, err error) { 66 d := deployUploader{ 67 applicationID: args.ApplicationID, 68 chID: args.CharmID, 69 csMac: args.CharmStoreMacaroon, 70 client: args.Client, 71 resources: args.ResourcesMeta, 72 osOpen: func(s string) (ReadSeekCloser, error) { return os.Open(s) }, 73 osStat: func(s string) error { _, err := os.Stat(s); return err }, 74 } 75 76 ids, err = d.upload(args.ResourceValues, args.Revisions) 77 if err != nil { 78 return nil, errors.Trace(err) 79 } 80 return ids, nil 81 } 82 83 type deployUploader struct { 84 applicationID string 85 chID charmstore.CharmID 86 csMac *macaroon.Macaroon 87 resources map[string]charmresource.Meta 88 client DeployClient 89 osOpen func(path string) (ReadSeekCloser, error) 90 osStat func(path string) error 91 } 92 93 func (d deployUploader) upload(resourceValues map[string]string, revisions map[string]int) (map[string]string, error) { 94 if err := d.validateResources(); err != nil { 95 return nil, errors.Trace(err) 96 } 97 98 if err := d.checkExpectedResources(resourceValues, revisions); err != nil { 99 return nil, errors.Trace(err) 100 } 101 102 if err := d.validateResourceDetails(resourceValues); err != nil { 103 return nil, errors.Trace(err) 104 } 105 106 storeResources := d.charmStoreResources(resourceValues, revisions) 107 pending := map[string]string{} 108 if len(storeResources) > 0 { 109 ids, err := d.client.AddPendingResources(d.applicationID, d.chID, d.csMac, storeResources) 110 if err != nil { 111 return nil, errors.Trace(err) 112 } 113 // guaranteed 1:1 correlation between ids and resources. 114 for i, res := range storeResources { 115 pending[res.Name] = ids[i] 116 } 117 } 118 119 for name, resValue := range resourceValues { 120 var ( 121 id string 122 err error 123 ) 124 switch d.resources[name].Type { 125 case charmresource.TypeFile: 126 id, err = d.uploadFile(name, resValue) 127 case charmresource.TypeContainerImage: 128 id, err = d.uploadDockerDetails(name, resValue) 129 default: 130 err = errors.New("unknown resource type to upload") 131 } 132 133 if err != nil { 134 return nil, errors.Trace(err) 135 } 136 pending[name] = id 137 } 138 139 return pending, nil 140 } 141 142 func (d deployUploader) validateResourceDetails(res map[string]string) error { 143 for name, value := range res { 144 var err error 145 switch d.resources[name].Type { 146 case charmresource.TypeFile: 147 err = d.checkFile(name, value) 148 case charmresource.TypeContainerImage: 149 dockerDetails, err := getDockerDetailsData(value) 150 if err != nil { 151 return err 152 } 153 // At the moment this is the same validation that occurs in getDockerDetailsData 154 err = resources.CheckDockerDetails(name, dockerDetails) 155 default: 156 return fmt.Errorf("unknown resource: %s", name) 157 } 158 if err != nil { 159 return err 160 } 161 } 162 return nil 163 } 164 165 func (d deployUploader) checkFile(name, path string) error { 166 err := d.osStat(path) 167 if os.IsNotExist(err) { 168 return errors.Annotatef(err, "file for resource %q", name) 169 } 170 if err != nil { 171 return errors.Annotatef(err, "can't read file for resource %q", name) 172 } 173 return nil 174 } 175 176 func (d deployUploader) validateResources() error { 177 var errs []error 178 for _, meta := range d.resources { 179 if err := meta.Validate(); err != nil { 180 errs = append(errs, err) 181 } 182 } 183 if len(errs) == 1 { 184 return errors.Trace(errs[0]) 185 } 186 if len(errs) > 1 { 187 msgs := make([]string, len(errs)) 188 for i, err := range errs { 189 msgs[i] = err.Error() 190 } 191 return errors.NewNotValid(nil, strings.Join(msgs, ", ")) 192 } 193 return nil 194 } 195 196 // charmStoreResources returns which resources revisions will need to be retrieved 197 // either as they where explicitly requested by the user for that rev or they 198 // weren't provided by the user. 199 func (d deployUploader) charmStoreResources(uploads map[string]string, revisions map[string]int) []charmresource.Resource { 200 var resources []charmresource.Resource 201 for name, meta := range d.resources { 202 if _, ok := uploads[name]; ok { 203 continue 204 } 205 206 revision := -1 207 if rev, ok := revisions[name]; ok { 208 revision = rev 209 } 210 211 resources = append(resources, charmresource.Resource{ 212 Meta: meta, 213 Origin: charmresource.OriginStore, 214 Revision: revision, 215 // Fingerprint and Size will be added server-side in 216 // the AddPendingResources() API call. 217 }) 218 } 219 return resources 220 } 221 222 func (d deployUploader) uploadPendingResource(resourcename, resourcevalue string, data io.ReadSeeker) (id string, err error) { 223 res := charmresource.Resource{ 224 Meta: d.resources[resourcename], 225 Origin: charmresource.OriginUpload, 226 } 227 228 return d.client.UploadPendingResource(d.applicationID, res, resourcevalue, data) 229 } 230 231 func (d deployUploader) uploadFile(resourcename, filename string) (id string, err error) { 232 f, err := d.osOpen(filename) 233 if err != nil { 234 return "", errors.Trace(err) 235 } 236 defer f.Close() 237 238 id, err = d.uploadPendingResource(resourcename, filename, f) 239 if err != nil { 240 return "", errors.Trace(err) 241 } 242 return id, err 243 } 244 245 func (d deployUploader) uploadDockerDetails(resourcename, registryPath string) (id string, error error) { 246 dockerDetails, err := getDockerDetailsData(registryPath) 247 if err != nil { 248 return "", errors.Trace(err) 249 } 250 data, err := json.Marshal(dockerDetails) 251 if err != nil { 252 return "", errors.Trace(err) 253 } 254 dr := bytes.NewReader(data) 255 256 id, err = d.uploadPendingResource(resourcename, registryPath, dr) 257 if err != nil { 258 return "", errors.Trace(err) 259 } 260 return id, nil 261 } 262 263 func (d deployUploader) checkExpectedResources(filenames map[string]string, revisions map[string]int) error { 264 var unknown []string 265 for name := range filenames { 266 if _, ok := d.resources[name]; !ok { 267 unknown = append(unknown, name) 268 } 269 } 270 for name := range revisions { 271 if _, ok := d.resources[name]; !ok { 272 unknown = append(unknown, name) 273 } 274 } 275 if len(unknown) == 1 { 276 return errors.Errorf("unrecognized resource %q", unknown[0]) 277 } 278 if len(unknown) > 1 { 279 return errors.Errorf("unrecognized resources: %s", strings.Join(unknown, ", ")) 280 } 281 return nil 282 } 283 284 // getDockerDetailsData determines if path is a local file path and extracts the 285 // details from that otherwise path is considered to be a registry path. 286 func getDockerDetailsData(path string) (resources.DockerImageDetails, error) { 287 f, err := os.Open(path) 288 if err == nil { 289 defer f.Close() 290 details, err := unMarshalDockerDetails(f) 291 if err != nil { 292 return details, errors.Trace(err) 293 } 294 return details, nil 295 } else if err := resources.ValidateDockerRegistryPath(path); err == nil { 296 return resources.DockerImageDetails{ 297 RegistryPath: path, 298 }, nil 299 } 300 return resources.DockerImageDetails{}, errors.NotValidf("filepath or registry path: %s", path) 301 302 } 303 304 func unMarshalDockerDetails(data io.Reader) (resources.DockerImageDetails, error) { 305 var details resources.DockerImageDetails 306 contents, err := ioutil.ReadAll(data) 307 if err != nil { 308 return details, errors.Trace(err) 309 } 310 311 if err := json.Unmarshal(contents, &details); err != nil { 312 if err := yaml.Unmarshal(contents, &details); err != nil { 313 return details, errors.Annotate(err, "file neither valid json or yaml") 314 } 315 } 316 if err := resources.ValidateDockerRegistryPath(details.RegistryPath); err != nil { 317 return resources.DockerImageDetails{}, err 318 } 319 return details, nil 320 }