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