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  }