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  }