1
// Copyright 2015 Canonical Ltd. All rights reserved.
16
"github.com/juju/errors"
17
"gopkg.in/macaroon-bakery.v1/httpbakery"
18
"launchpad.net/gnuflag"
20
"github.com/juju/juju/api"
21
"github.com/juju/juju/api/charms"
24
var budgetWithLimitRe = regexp.MustCompile(`^[a-zA-Z0-9\-]+:[0-9]+$`)
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"`
35
// RegisterMeteredCharm implements the DeployStep interface.
36
type RegisterMeteredCharm struct {
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")
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
58
return errors.Trace(err)
61
charmsClient := charms.NewClient(state)
62
metered, err := charmsClient.IsMetered(deployInfo.CharmID.URL.String())
70
if r.Plan == "" && deployInfo.CharmID.URL.Schema == "cs" {
71
r.Plan, err = r.getDefaultPlan(bakeryClient, deployInfo.CharmID.URL.String())
73
if isNoDefaultPlanError(err) {
74
options, err1 := r.getCharmPlans(bakeryClient, deployInfo.CharmID.URL.String())
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, ", "))
85
r.credentials, err = r.registerMetrics(
87
deployInfo.CharmID.URL.String(),
88
deployInfo.ApplicationName,
93
if deployInfo.CharmID.URL.Schema == "cs" {
94
logger.Infof("failed to obtain plan authorization: %v", err)
97
logger.Debugf("no plan authorization: %v", err)
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 {
107
if r.credentials == nil {
110
api, cerr := getMetricCredentialsAPI(state)
112
logger.Infof("failed to get the metrics credentials setter: %v", cerr)
117
err := api.SetMetricCredentials(deployInfo.ApplicationName, r.credentials)
119
logger.Warningf("failed to set metric credentials: %v", err)
120
return errors.Trace(err)
126
type noDefaultPlanError struct {
130
func (e *noDefaultPlanError) Error() string {
131
return fmt.Sprintf("%v has no default plan", e.cUrl)
134
func isNoDefaultPlanError(e error) bool {
135
_, ok := e.(*noDefaultPlanError)
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")
144
qURL, err := url.Parse(r.QueryURL + "/default")
146
return "", errors.Trace(err)
149
query := qURL.Query()
150
query.Set("charm-url", cURL)
151
qURL.RawQuery = query.Encode()
153
req, err := http.NewRequest("GET", qURL.String(), nil)
155
return "", errors.Trace(err)
158
response, err := client.Do(req)
160
return "", errors.Trace(err)
162
defer response.Body.Close()
164
if response.StatusCode == http.StatusNotFound {
165
return "", &noDefaultPlanError{cURL}
167
if response.StatusCode != http.StatusOK {
168
return "", errors.Errorf("failed to query default plan: http response is %d", response.StatusCode)
171
var planInfo struct {
172
URL string `json:"url"`
174
dec := json.NewDecoder(response.Body)
175
err = dec.Decode(&planInfo)
177
return "", errors.Trace(err)
179
return planInfo.URL, nil
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")
186
qURL, err := url.Parse(r.QueryURL)
188
return nil, errors.Trace(err)
191
query := qURL.Query()
192
query.Set("charm-url", cURL)
193
qURL.RawQuery = query.Encode()
195
req, err := http.NewRequest("GET", qURL.String(), nil)
197
return nil, errors.Trace(err)
200
response, err := client.Do(req)
202
return nil, errors.Trace(err)
204
defer response.Body.Close()
206
if response.StatusCode != http.StatusOK {
207
return nil, errors.Errorf("failed to query plans: http response is %d", response.StatusCode)
210
var planInfo []struct {
211
URL string `json:"url"`
213
dec := json.NewDecoder(response.Body)
214
err = dec.Decode(&planInfo)
216
return nil, errors.Trace(err)
218
info := make([]string, len(planInfo))
219
for i, p := range planInfo {
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")
229
registerURL, err := url.Parse(r.RegisterURL)
231
return nil, errors.Trace(err)
234
registrationPost := metricRegistrationPost{
235
ModelUUID: modelUUID,
237
ApplicationName: serviceName,
243
buff := &bytes.Buffer{}
244
encoder := json.NewEncoder(buff)
245
err = encoder.Encode(registrationPost)
247
return nil, errors.Trace(err)
250
req, err := http.NewRequest("POST", registerURL.String(), nil)
252
return nil, errors.Trace(err)
254
req.Header.Set("Content-Type", "application/json")
256
response, err := client.DoWithBody(req, bytes.NewReader(buff.Bytes()))
258
return nil, errors.Trace(err)
260
defer response.Body.Close()
262
if response.StatusCode == http.StatusOK {
263
b, err := ioutil.ReadAll(response.Body)
265
return nil, errors.Annotatef(err, "failed to read the response")
269
var respError struct {
270
Error string `json:"error"`
272
err = json.NewDecoder(response.Body).Decode(&respError)
274
return nil, errors.Errorf("authorization failed: http response is %d", response.StatusCode)
276
return nil, errors.Errorf("authorization failed: %s", respError.Error)
279
func parseBudgetWithLimit(bl string) (string, string, error) {
280
if !budgetWithLimitRe.MatchString(bl) {
281
return "", "", errors.New("invalid allocation, expecting <budget>:<limit>")
283
parts := strings.Split(bl, ":")
284
return parts[0], parts[1], nil