1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
11
"github.com/juju/errors"
12
charmresource "gopkg.in/juju/charm.v6-unstable/resource"
13
"gopkg.in/macaroon.v1"
15
"github.com/juju/juju/charmstore"
18
// DeployClient exposes the functionality of the resources API needed
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)
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)
28
// DeployResourcesArgs holds the arguments to DeployResources().
29
type DeployResourcesArgs struct {
30
// ApplicationID identifies the application being deployed.
33
// CharmID identifies the application's charm.
34
CharmID charmstore.CharmID
36
// CharmStoreMacaroon is the macaroon to use for the charm when
37
// interacting with the charm store.
38
CharmStoreMacaroon *macaroon.Macaroon
40
// Filenames is the set of resources for which a filename
41
// was provided at the command-line.
42
Filenames map[string]string
44
// Revisions is the set of resources for which a revision
45
// was provided at the command-line.
46
Revisions map[string]int
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
52
// Client is the resources API client to use during deploy.
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) {
61
applicationID: args.ApplicationID,
63
csMac: args.CharmStoreMacaroon,
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 },
70
ids, err = d.upload(args.Filenames, args.Revisions)
72
return nil, errors.Trace(err)
77
type deployUploader struct {
79
chID charmstore.CharmID
80
csMac *macaroon.Macaroon
81
resources map[string]charmresource.Meta
83
osOpen func(path string) (ReadSeekCloser, error)
84
osStat func(path string) error
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)
92
if err := d.checkExpectedResources(files, revisions); err != nil {
93
return nil, errors.Trace(err)
96
if err := d.checkFiles(files); err != nil {
97
return nil, errors.Trace(err)
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)
105
return nil, errors.Trace(err)
107
// guaranteed 1:1 correlation between ids and resources.
108
for i, res := range storeResources {
109
pending[res.Name] = ids[i]
113
for name, filename := range files {
114
id, err := d.uploadFile(name, filename)
116
return nil, errors.Trace(err)
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)
131
return errors.Annotatef(err, "can't read file for resource %q", name)
137
func (d deployUploader) validateResources() error {
139
for _, meta := range d.resources {
140
if err := meta.Validate(); err != nil {
141
errs = append(errs, err)
145
return errors.Trace(errs[0])
148
msgs := make([]string, len(errs))
149
for i, err := range errs {
150
msgs[i] = err.Error()
152
return errors.NewNotValid(nil, strings.Join(msgs, ", "))
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 {
165
if rev, ok := revisions[name]; ok {
169
resources = append(resources, charmresource.Resource{
171
Origin: charmresource.OriginStore,
173
// Fingerprint and Size will be added server-side in
174
// the AddPendingResources() API call.
180
func (d deployUploader) uploadFile(resourcename, filename string) (id string, err error) {
181
f, err := d.osOpen(filename)
183
return "", errors.Trace(err)
186
res := charmresource.Resource{
187
Meta: d.resources[resourcename],
188
Origin: charmresource.OriginUpload,
191
id, err = d.client.AddPendingResource(d.applicationID, res, filename, f)
193
return "", errors.Trace(err)
198
func (d deployUploader) checkExpectedResources(filenames map[string]string, revisions map[string]int) error {
200
for name := range filenames {
201
if _, ok := d.resources[name]; !ok {
202
unknown = append(unknown, name)
205
for name := range revisions {
206
if _, ok := d.resources[name]; !ok {
207
unknown = append(unknown, name)
210
if len(unknown) == 1 {
211
return errors.Errorf("unrecognized resource %q", unknown[0])
213
if len(unknown) > 1 {
214
return errors.Errorf("unrecognized resources: %s", strings.Join(unknown, ", "))