github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/service/register.go (about) 1 // Copyright 2015 Canonical Ltd. All rights reserved. 2 3 package service 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 ServiceName 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.ServiceName, 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.ServiceName, 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(environmentUUID, 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: environmentUUID, 236 CharmURL: charmURL, 237 ServiceName: 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 }