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