~juju-qa/ubuntu/yakkety/juju/2.0-beta10

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/cmd/juju/application/register.go

  • Committer: Martin Packman
  • Date: 2016-06-28 11:15:00 UTC
  • mfrom: (1.4.3)
  • Revision ID: martin.packman@canonical.com-20160628111500-3vabmx2cls3plbp1
Merge new upstream source 2.0~beta10

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd. All rights reserved.
 
2
 
 
3
package application
 
4
 
 
5
import (
 
6
        "bytes"
 
7
        "encoding/json"
 
8
        "fmt"
 
9
        "io/ioutil"
 
10
        "net/http"
 
11
        "net/url"
 
12
        "regexp"
 
13
        "strings"
 
14
 
 
15
        "github.com/juju/cmd"
 
16
        "github.com/juju/errors"
 
17
        "gopkg.in/macaroon-bakery.v1/httpbakery"
 
18
        "launchpad.net/gnuflag"
 
19
 
 
20
        "github.com/juju/juju/api"
 
21
        "github.com/juju/juju/api/charms"
 
22
)
 
23
 
 
24
var budgetWithLimitRe = regexp.MustCompile(`^[a-zA-Z0-9\-]+:[0-9]+$`)
 
25
 
 
26
type metricRegistrationPost struct {
 
27
        ModelUUID       string `json:"env-uuid"`
 
28
        CharmURL        string `json:"charm-url"`
 
29
        ApplicationName string `json:"service-name"`
 
30
        PlanURL         string `json:"plan-url"`
 
31
        Budget          string `json:"budget"`
 
32
        Limit           string `json:"limit"`
 
33
}
 
34
 
 
35
// RegisterMeteredCharm implements the DeployStep interface.
 
36
type RegisterMeteredCharm struct {
 
37
        AllocationSpec string
 
38
        Plan           string
 
39
        RegisterURL    string
 
40
        QueryURL       string
 
41
        credentials    []byte
 
42
        budget         string
 
43
        limit          string
 
44
}
 
45
 
 
46
func (r *RegisterMeteredCharm) SetFlags(f *gnuflag.FlagSet) {
 
47
        f.StringVar(&r.AllocationSpec, "budget", "personal:0", "budget and allocation limit")
 
48
        f.StringVar(&r.Plan, "plan", "", "plan to deploy charm under")
 
49
}
 
50
 
 
51
// RunPre obtains authorization to deploy this charm. The authorization, if received is not
 
52
// sent to the controller, rather it is kept as an attribute on RegisterMeteredCharm.
 
53
func (r *RegisterMeteredCharm) RunPre(state api.Connection, bakeryClient *httpbakery.Client, ctx *cmd.Context, deployInfo DeploymentInfo) error {
 
54
        if allocBudget, allocLimit, err := parseBudgetWithLimit(r.AllocationSpec); err == nil {
 
55
                // Make these available to registration if valid.
 
56
                r.budget, r.limit = allocBudget, allocLimit
 
57
        } else {
 
58
                return errors.Trace(err)
 
59
        }
 
60
 
 
61
        charmsClient := charms.NewClient(state)
 
62
        metered, err := charmsClient.IsMetered(deployInfo.CharmID.URL.String())
 
63
        if err != nil {
 
64
                return err
 
65
        }
 
66
        if !metered {
 
67
                return nil
 
68
        }
 
69
 
 
70
        if r.Plan == "" && deployInfo.CharmID.URL.Schema == "cs" {
 
71
                r.Plan, err = r.getDefaultPlan(bakeryClient, deployInfo.CharmID.URL.String())
 
72
                if err != nil {
 
73
                        if isNoDefaultPlanError(err) {
 
74
                                options, err1 := r.getCharmPlans(bakeryClient, deployInfo.CharmID.URL.String())
 
75
                                if err1 != nil {
 
76
                                        return err1
 
77
                                }
 
78
                                charmUrl := deployInfo.CharmID.URL.String()
 
79
                                return errors.Errorf(`%v has no default plan. Try "juju deploy --plan <plan-name> with one of %v"`, charmUrl, strings.Join(options, ", "))
 
80
                        }
 
81
                        return err
 
82
                }
 
83
        }
 
84
 
 
85
        r.credentials, err = r.registerMetrics(
 
86
                deployInfo.ModelUUID,
 
87
                deployInfo.CharmID.URL.String(),
 
88
                deployInfo.ApplicationName,
 
89
                r.budget,
 
90
                r.limit,
 
91
                bakeryClient)
 
92
        if err != nil {
 
93
                if deployInfo.CharmID.URL.Schema == "cs" {
 
94
                        logger.Infof("failed to obtain plan authorization: %v", err)
 
95
                        return err
 
96
                }
 
97
                logger.Debugf("no plan authorization: %v", err)
 
98
        }
 
99
        return nil
 
100
}
 
101
 
 
102
// RunPost sends credentials obtained during the call to RunPre to the controller.
 
103
func (r *RegisterMeteredCharm) RunPost(state api.Connection, bakeryClient *httpbakery.Client, ctx *cmd.Context, deployInfo DeploymentInfo, prevErr error) error {
 
104
        if prevErr != nil {
 
105
                return nil
 
106
        }
 
107
        if r.credentials == nil {
 
108
                return nil
 
109
        }
 
110
        api, cerr := getMetricCredentialsAPI(state)
 
111
        if cerr != nil {
 
112
                logger.Infof("failed to get the metrics credentials setter: %v", cerr)
 
113
                return cerr
 
114
        }
 
115
        defer api.Close()
 
116
 
 
117
        err := api.SetMetricCredentials(deployInfo.ApplicationName, r.credentials)
 
118
        if err != nil {
 
119
                logger.Warningf("failed to set metric credentials: %v", err)
 
120
                return errors.Trace(err)
 
121
        }
 
122
 
 
123
        return nil
 
124
}
 
125
 
 
126
type noDefaultPlanError struct {
 
127
        cUrl string
 
128
}
 
129
 
 
130
func (e *noDefaultPlanError) Error() string {
 
131
        return fmt.Sprintf("%v has no default plan", e.cUrl)
 
132
}
 
133
 
 
134
func isNoDefaultPlanError(e error) bool {
 
135
        _, ok := e.(*noDefaultPlanError)
 
136
        return ok
 
137
}
 
138
 
 
139
func (r *RegisterMeteredCharm) getDefaultPlan(client *httpbakery.Client, cURL string) (string, error) {
 
140
        if r.QueryURL == "" {
 
141
                return "", errors.Errorf("no plan query url specified")
 
142
        }
 
143
 
 
144
        qURL, err := url.Parse(r.QueryURL + "/default")
 
145
        if err != nil {
 
146
                return "", errors.Trace(err)
 
147
        }
 
148
 
 
149
        query := qURL.Query()
 
150
        query.Set("charm-url", cURL)
 
151
        qURL.RawQuery = query.Encode()
 
152
 
 
153
        req, err := http.NewRequest("GET", qURL.String(), nil)
 
154
        if err != nil {
 
155
                return "", errors.Trace(err)
 
156
        }
 
157
 
 
158
        response, err := client.Do(req)
 
159
        if err != nil {
 
160
                return "", errors.Trace(err)
 
161
        }
 
162
        defer response.Body.Close()
 
163
 
 
164
        if response.StatusCode == http.StatusNotFound {
 
165
                return "", &noDefaultPlanError{cURL}
 
166
        }
 
167
        if response.StatusCode != http.StatusOK {
 
168
                return "", errors.Errorf("failed to query default plan: http response is %d", response.StatusCode)
 
169
        }
 
170
 
 
171
        var planInfo struct {
 
172
                URL string `json:"url"`
 
173
        }
 
174
        dec := json.NewDecoder(response.Body)
 
175
        err = dec.Decode(&planInfo)
 
176
        if err != nil {
 
177
                return "", errors.Trace(err)
 
178
        }
 
179
        return planInfo.URL, nil
 
180
}
 
181
 
 
182
func (r *RegisterMeteredCharm) getCharmPlans(client *httpbakery.Client, cURL string) ([]string, error) {
 
183
        if r.QueryURL == "" {
 
184
                return nil, errors.Errorf("no plan query url specified")
 
185
        }
 
186
        qURL, err := url.Parse(r.QueryURL)
 
187
        if err != nil {
 
188
                return nil, errors.Trace(err)
 
189
        }
 
190
 
 
191
        query := qURL.Query()
 
192
        query.Set("charm-url", cURL)
 
193
        qURL.RawQuery = query.Encode()
 
194
 
 
195
        req, err := http.NewRequest("GET", qURL.String(), nil)
 
196
        if err != nil {
 
197
                return nil, errors.Trace(err)
 
198
        }
 
199
 
 
200
        response, err := client.Do(req)
 
201
        if err != nil {
 
202
                return nil, errors.Trace(err)
 
203
        }
 
204
        defer response.Body.Close()
 
205
 
 
206
        if response.StatusCode != http.StatusOK {
 
207
                return nil, errors.Errorf("failed to query plans: http response is %d", response.StatusCode)
 
208
        }
 
209
 
 
210
        var planInfo []struct {
 
211
                URL string `json:"url"`
 
212
        }
 
213
        dec := json.NewDecoder(response.Body)
 
214
        err = dec.Decode(&planInfo)
 
215
        if err != nil {
 
216
                return nil, errors.Trace(err)
 
217
        }
 
218
        info := make([]string, len(planInfo))
 
219
        for i, p := range planInfo {
 
220
                info[i] = p.URL
 
221
        }
 
222
        return info, nil
 
223
}
 
224
 
 
225
func (r *RegisterMeteredCharm) registerMetrics(modelUUID, charmURL, serviceName, budget, limit string, client *httpbakery.Client) ([]byte, error) {
 
226
        if r.RegisterURL == "" {
 
227
                return nil, errors.Errorf("no metric registration url is specified")
 
228
        }
 
229
        registerURL, err := url.Parse(r.RegisterURL)
 
230
        if err != nil {
 
231
                return nil, errors.Trace(err)
 
232
        }
 
233
 
 
234
        registrationPost := metricRegistrationPost{
 
235
                ModelUUID:       modelUUID,
 
236
                CharmURL:        charmURL,
 
237
                ApplicationName: serviceName,
 
238
                PlanURL:         r.Plan,
 
239
                Budget:          budget,
 
240
                Limit:           limit,
 
241
        }
 
242
 
 
243
        buff := &bytes.Buffer{}
 
244
        encoder := json.NewEncoder(buff)
 
245
        err = encoder.Encode(registrationPost)
 
246
        if err != nil {
 
247
                return nil, errors.Trace(err)
 
248
        }
 
249
 
 
250
        req, err := http.NewRequest("POST", registerURL.String(), nil)
 
251
        if err != nil {
 
252
                return nil, errors.Trace(err)
 
253
        }
 
254
        req.Header.Set("Content-Type", "application/json")
 
255
 
 
256
        response, err := client.DoWithBody(req, bytes.NewReader(buff.Bytes()))
 
257
        if err != nil {
 
258
                return nil, errors.Trace(err)
 
259
        }
 
260
        defer response.Body.Close()
 
261
 
 
262
        if response.StatusCode == http.StatusOK {
 
263
                b, err := ioutil.ReadAll(response.Body)
 
264
                if err != nil {
 
265
                        return nil, errors.Annotatef(err, "failed to read the response")
 
266
                }
 
267
                return b, nil
 
268
        }
 
269
        var respError struct {
 
270
                Error string `json:"error"`
 
271
        }
 
272
        err = json.NewDecoder(response.Body).Decode(&respError)
 
273
        if err != nil {
 
274
                return nil, errors.Errorf("authorization failed: http response is %d", response.StatusCode)
 
275
        }
 
276
        return nil, errors.Errorf("authorization failed: %s", respError.Error)
 
277
}
 
278
 
 
279
func parseBudgetWithLimit(bl string) (string, string, error) {
 
280
        if !budgetWithLimitRe.MatchString(bl) {
 
281
                return "", "", errors.New("invalid allocation, expecting <budget>:<limit>")
 
282
        }
 
283
        parts := strings.Split(bl, ":")
 
284
        return parts[0], parts[1], nil
 
285
}