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  }