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 }