github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/addrelation.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application
     5  
     6  import (
     7  	"net"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/gnuflag"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/api/application"
    17  	"github.com/juju/juju/api/applicationoffers"
    18  	"github.com/juju/juju/apiserver/params"
    19  	jujucmd "github.com/juju/juju/cmd"
    20  	"github.com/juju/juju/cmd/juju/block"
    21  	"github.com/juju/juju/cmd/juju/common"
    22  	"github.com/juju/juju/cmd/modelcmd"
    23  	"github.com/juju/juju/core/crossmodel"
    24  )
    25  
    26  const addRelationDoc = `
    27  Add a relation between 2 local application endpoints or a local endpoint and a remote application endpoint.
    28  Adding a relation between two remote application endpoints is not supported.
    29  
    30  Application endpoints can be identified either by:
    31      <application name>[:<relation name>]
    32          where application name supplied without relation will be internally expanded to be well-formed
    33  or
    34      <model name>.<application name>[:<relation name>]
    35          where the application is hosted in another model owned by the current user, in the same controller
    36  or
    37      <user name>/<model name>.<application name>[:<relation name>]
    38          where user/model is another model in the same controller
    39  
    40  For a cross model relation, if the consuming side is behind a firewall and/or NAT is used for outbound traffic,
    41  it is possible to use the --via option to inform the offering side the source of traffic so that any required
    42  firewall ports may be opened.
    43  
    44  Examples:
    45      $ juju add-relation wordpress mysql
    46          where "wordpress" and "mysql" will be internally expanded to "wordpress:db" and "mysql:server" respectively
    47  
    48      $ juju add-relation wordpress someone/prod.mysql
    49          where "wordpress" will be internally expanded to "wordpress:db"
    50  
    51      $ juju add-relation wordpress someone/prod.mysql --via 192.168.0.0/16
    52      
    53      $ juju add-relation wordpress someone/prod.mysql --via 192.168.0.0/16,10.0.0.0/8
    54  
    55  `
    56  
    57  var localEndpointRegEx = regexp.MustCompile("^" + names.RelationSnippet + "$")
    58  
    59  // NewAddRelationCommand returns a command to add a relation between 2 applications.
    60  func NewAddRelationCommand() cmd.Command {
    61  	return modelcmd.Wrap(&addRelationCommand{})
    62  }
    63  
    64  // addRelationCommand adds a relation between two application endpoints.
    65  type addRelationCommand struct {
    66  	modelcmd.ModelCommandBase
    67  	endpoints         []string
    68  	viaCIDRs          []string
    69  	viaValue          string
    70  	remoteEndpoint    *crossmodel.OfferURL
    71  	addRelationAPI    applicationAddRelationAPI
    72  	consumeDetailsAPI applicationConsumeDetailsAPI
    73  }
    74  
    75  func (c *addRelationCommand) Info() *cmd.Info {
    76  	addCmd := &cmd.Info{
    77  		Name:    "add-relation",
    78  		Aliases: []string{"relate"},
    79  		Args:    "<application1>[:<endpoint name1>] <application2>[:<endpoint name2>]",
    80  		Purpose: "Add a relation between two application endpoints.",
    81  		Doc:     addRelationDoc,
    82  	}
    83  	return jujucmd.Info(addCmd)
    84  }
    85  
    86  func (c *addRelationCommand) Init(args []string) error {
    87  	if len(args) != 2 {
    88  		return errors.Errorf("a relation must involve two applications")
    89  	}
    90  	if err := c.validateEndpoints(args); err != nil {
    91  		return err
    92  	}
    93  	if err := c.validateCIDRs(); err != nil {
    94  		return err
    95  	}
    96  	if c.remoteEndpoint == nil && len(c.viaCIDRs) > 0 {
    97  		return errors.New("the --via option can only be used when relating to offers in a different model")
    98  	}
    99  	return nil
   100  }
   101  
   102  func (c *addRelationCommand) SetFlags(f *gnuflag.FlagSet) {
   103  	f.StringVar(&c.viaValue, "via", "", "for cross model relations, specify the egress subnets for outbound traffic")
   104  }
   105  
   106  // applicationAddRelationAPI defines the API methods that application add relation command uses.
   107  type applicationAddRelationAPI interface {
   108  	Close() error
   109  	BestAPIVersion() int
   110  	AddRelation(endpoints, viaCIDRs []string) (*params.AddRelationResults, error)
   111  	Consume(crossmodel.ConsumeApplicationArgs) (string, error)
   112  }
   113  
   114  func (c *addRelationCommand) getAddRelationAPI() (applicationAddRelationAPI, error) {
   115  	if c.addRelationAPI != nil {
   116  		return c.addRelationAPI, nil
   117  	}
   118  
   119  	root, err := c.NewAPIRoot()
   120  	if err != nil {
   121  		return nil, errors.Trace(err)
   122  	}
   123  	return application.NewClient(root), nil
   124  }
   125  
   126  func (c *addRelationCommand) getOffersAPI(url *crossmodel.OfferURL) (applicationConsumeDetailsAPI, error) {
   127  	if c.consumeDetailsAPI != nil {
   128  		return c.consumeDetailsAPI, nil
   129  	}
   130  
   131  	root, err := c.CommandBase.NewAPIRoot(c.ClientStore(), url.Source, "")
   132  	if err != nil {
   133  		return nil, errors.Trace(err)
   134  	}
   135  	return applicationoffers.NewClient(root), nil
   136  }
   137  
   138  func (c *addRelationCommand) Run(ctx *cmd.Context) error {
   139  	client, err := c.getAddRelationAPI()
   140  	if err != nil {
   141  		return err
   142  	}
   143  	defer client.Close()
   144  
   145  	if c.remoteEndpoint != nil {
   146  		if client.BestAPIVersion() < 5 {
   147  			// old client does not have cross-model capability.
   148  			return errors.NotSupportedf("cannot add relation to %s: remote endpoints", c.remoteEndpoint.String())
   149  		}
   150  		if c.remoteEndpoint.Source == "" {
   151  			var err error
   152  			controllerName, err := c.ControllerName()
   153  			if err != nil {
   154  				return errors.Trace(err)
   155  			}
   156  			c.remoteEndpoint.Source = controllerName
   157  		}
   158  		if err := c.maybeConsumeOffer(client); err != nil {
   159  			return errors.Trace(err)
   160  		}
   161  	}
   162  
   163  	_, err = client.AddRelation(c.endpoints, c.viaCIDRs)
   164  	if params.IsCodeUnauthorized(err) {
   165  		common.PermissionsMessage(ctx.Stderr, "add a relation")
   166  	}
   167  	if params.IsCodeAlreadyExists(err) {
   168  		// It's not a real error, mention about it, log it and move along
   169  		logger.Infof("%s", err)
   170  		ctx.Infof("%s", err)
   171  		err = nil
   172  	}
   173  	return block.ProcessBlockedError(err, block.BlockChange)
   174  }
   175  
   176  func (c *addRelationCommand) maybeConsumeOffer(targetClient applicationAddRelationAPI) error {
   177  	sourceClient, err := c.getOffersAPI(c.remoteEndpoint)
   178  	if err != nil {
   179  		return errors.Trace(err)
   180  	}
   181  	defer sourceClient.Close()
   182  
   183  	// Get the details of the remote offer - this will fail with a permission
   184  	// error if the user isn't authorised to consume the offer.
   185  	consumeDetails, err := sourceClient.GetConsumeDetails(c.remoteEndpoint.AsLocal().String())
   186  	if err != nil {
   187  		return errors.Trace(err)
   188  	}
   189  	// Parse the offer details URL and add the source controller so
   190  	// things like status can show the original source of the offer.
   191  	offerURL, err := crossmodel.ParseOfferURL(consumeDetails.Offer.OfferURL)
   192  	if err != nil {
   193  		return errors.Trace(err)
   194  	}
   195  	offerURL.Source = c.remoteEndpoint.Source
   196  	consumeDetails.Offer.OfferURL = offerURL.String()
   197  
   198  	// Consume is idempotent so even if the offer has been consumed previously,
   199  	// it's safe to do so again.
   200  	arg := crossmodel.ConsumeApplicationArgs{
   201  		Offer:            *consumeDetails.Offer,
   202  		ApplicationAlias: c.remoteEndpoint.ApplicationName,
   203  		Macaroon:         consumeDetails.Macaroon,
   204  	}
   205  	if consumeDetails.ControllerInfo != nil {
   206  		controllerTag, err := names.ParseControllerTag(consumeDetails.ControllerInfo.ControllerTag)
   207  		if err != nil {
   208  			return errors.Trace(err)
   209  		}
   210  		arg.ControllerInfo = &crossmodel.ControllerInfo{
   211  			ControllerTag: controllerTag,
   212  			Alias:         offerURL.Source,
   213  			Addrs:         consumeDetails.ControllerInfo.Addrs,
   214  			CACert:        consumeDetails.ControllerInfo.CACert,
   215  		}
   216  	}
   217  	_, err = targetClient.Consume(arg)
   218  	return errors.Trace(err)
   219  }
   220  
   221  // validateEndpoints determines if all endpoints are valid.
   222  // Each endpoint is either from local application or remote.
   223  // If more than one remote endpoint are supplied, the input argument are considered invalid.
   224  func (c *addRelationCommand) validateEndpoints(all []string) error {
   225  	for _, endpoint := range all {
   226  		// We can only determine if this is a remote endpoint with 100%.
   227  		// If we cannot parse it, it may still be a valid local endpoint...
   228  		// so ignoring parsing error,
   229  		if url, err := crossmodel.ParseOfferURL(endpoint); err == nil {
   230  			if c.remoteEndpoint != nil {
   231  				return errors.NotSupportedf("providing more than one remote endpoints")
   232  			}
   233  			c.remoteEndpoint = url
   234  			c.endpoints = append(c.endpoints, url.ApplicationName)
   235  			continue
   236  		}
   237  		// at this stage, we are assuming that this could be a local endpoint
   238  		if err := validateLocalEndpoint(endpoint, ":"); err != nil {
   239  			return err
   240  		}
   241  		c.endpoints = append(c.endpoints, endpoint)
   242  	}
   243  	return nil
   244  }
   245  
   246  // validateLocalEndpoint determines if given endpoint could be a valid
   247  func validateLocalEndpoint(endpoint string, sep string) error {
   248  	i := strings.Index(endpoint, sep)
   249  	applicationName := endpoint
   250  	if i != -1 {
   251  		// not a valid endpoint as sep either at the start or the end of the name
   252  		if i == 0 || i == len(endpoint)-1 {
   253  			return errors.NotValidf("endpoint %q", endpoint)
   254  		}
   255  
   256  		parts := strings.SplitN(endpoint, sep, -1)
   257  		if rightCount := len(parts) == 2; !rightCount {
   258  			// not valid if there are not exactly 2 parts.
   259  			return errors.NotValidf("endpoint %q", endpoint)
   260  		}
   261  
   262  		applicationName = parts[0]
   263  
   264  		if valid := localEndpointRegEx.MatchString(parts[1]); !valid {
   265  			return errors.NotValidf("endpoint %q", endpoint)
   266  		}
   267  	}
   268  
   269  	if valid := names.IsValidApplication(applicationName); !valid {
   270  		return errors.NotValidf("application name %q", applicationName)
   271  	}
   272  	return nil
   273  }
   274  
   275  func (c *addRelationCommand) validateCIDRs() error {
   276  	if c.viaValue == "" {
   277  		return nil
   278  	}
   279  	c.viaCIDRs = strings.Split(
   280  		strings.Replace(c.viaValue, " ", "", -1), ",")
   281  	for _, cidr := range c.viaCIDRs {
   282  		if _, _, err := net.ParseCIDR(cidr); err != nil {
   283  			return err
   284  		}
   285  		if cidr == "0.0.0.0/0" {
   286  			return errors.Errorf("CIDR %q not allowed", cidr)
   287  		}
   288  	}
   289  	return nil
   290  }