~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/resource/cmd/deploy.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
}