github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/core/crossmodel/offerurl.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package crossmodel 5 6 import ( 7 "fmt" 8 "regexp" 9 "strings" 10 11 "github.com/juju/errors" 12 "gopkg.in/juju/names.v2" 13 ) 14 15 // OfferURL represents the location of an offered application and its 16 // associated exported endpoints. 17 type OfferURL struct { 18 // Source represents where the offer is hosted. 19 // If empty, the model is another model in the same controller. 20 Source string // "<controller-name>" or "<jaas>" or "" 21 22 // User is the user whose namespace in which the offer is made. 23 // Where a model is specified, the user is the model owner. 24 User string 25 26 // ModelName is the name of the model providing the exported endpoints. 27 // It is only used for local URLs or for specifying models in the same 28 // controller. 29 ModelName string 30 31 // ApplicationName is the name of the application providing the exported endpoints. 32 ApplicationName string 33 } 34 35 // Path returns the path component of the URL. 36 func (u *OfferURL) Path() string { 37 var parts []string 38 if u.User != "" { 39 parts = append(parts, u.User) 40 } 41 if u.ModelName != "" { 42 parts = append(parts, u.ModelName) 43 } 44 path := strings.Join(parts, "/") 45 path = fmt.Sprintf("%s.%s", path, u.ApplicationName) 46 if u.Source == "" { 47 return path 48 } 49 return fmt.Sprintf("%s:%s", u.Source, path) 50 } 51 52 func (u *OfferURL) String() string { 53 return u.Path() 54 } 55 56 // AsLocal returns a copy of the URL with an empty (local) source. 57 func (u *OfferURL) AsLocal() *OfferURL { 58 localURL := *u 59 localURL.Source = "" 60 return &localURL 61 } 62 63 // HasEndpoint returns whether this offer URL includes an 64 // endpoint name in the application name. 65 func (u *OfferURL) HasEndpoint() bool { 66 return strings.Contains(u.ApplicationName, ":") 67 } 68 69 // modelApplicationRegexp parses urls of the form controller:user/model.application[:relname] 70 var modelApplicationRegexp = regexp.MustCompile(`(/?((?P<user>[^/]+)/)?(?P<model>[^.]*)(\.(?P<application>[^:]*(:.*)?))?)?`) 71 72 //var modelApplicationRegexp = regexp.MustCompile(`(/?((?P<user>[a-zA-Z]+)/)?(?P<model>[a-zA-Z]+)?(\.(?P<application>[^:]*(:[a-zA-Z]+)?))?)?`) 73 74 // ParseOfferURL parses the specified URL string into an OfferURL. 75 // The URL string is of one of the forms: 76 // <model-name>.<application-name> 77 // <model-name>.<application-name>:<relation-name> 78 // <user>/<model-name>.<application-name> 79 // <user>/<model-name>.<application-name>:<relation-name> 80 // <controller>:<user>/<model-name>.<application-name> 81 // <controller>:<user>/<model-name>.<application-name>:<relation-name> 82 func ParseOfferURL(urlStr string) (*OfferURL, error) { 83 return parseOfferURL(urlStr) 84 } 85 86 // parseOfferURL parses the specified URL string into an OfferURL. 87 func parseOfferURL(urlStr string) (*OfferURL, error) { 88 urlParts, err := parseOfferURLParts(urlStr, false) 89 if err != nil { 90 return nil, err 91 } 92 url := OfferURL(*urlParts) 93 return &url, nil 94 } 95 96 // OfferURLParts contains various attributes of a URL. 97 type OfferURLParts OfferURL 98 99 // ParseOfferURLParts parses a partial URL, filling out what parts are supplied. 100 // This method is used to generate a filter used to query matching offer URLs. 101 func ParseOfferURLParts(urlStr string) (*OfferURLParts, error) { 102 return parseOfferURLParts(urlStr, true) 103 } 104 105 var endpointRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+$`) 106 107 func maybeParseSource(urlStr string) (source, rest string) { 108 parts := strings.Split(urlStr, ":") 109 switch len(parts) { 110 case 3: 111 return parts[0], parts[1] + ":" + parts[2] 112 case 2: 113 if endpointRegexp.MatchString(parts[1]) { 114 return "", urlStr 115 } 116 return parts[0], parts[1] 117 } 118 return "", urlStr 119 } 120 121 func parseOfferURLParts(urlStr string, allowIncomplete bool) (*OfferURLParts, error) { 122 var result OfferURLParts 123 source, urlParts := maybeParseSource(urlStr) 124 125 valid := !strings.HasPrefix(urlStr, ":") 126 valid = valid && modelApplicationRegexp.MatchString(urlParts) 127 if valid { 128 result.Source = source 129 result.User = modelApplicationRegexp.ReplaceAllString(urlParts, "$user") 130 result.ModelName = modelApplicationRegexp.ReplaceAllString(urlParts, "$model") 131 result.ApplicationName = modelApplicationRegexp.ReplaceAllString(urlParts, "$application") 132 } 133 if !valid || strings.Contains(result.ModelName, "/") || strings.Contains(result.ApplicationName, "/") { 134 // TODO(wallyworld) - update error message when we support multi-controller and JAAS CMR 135 return nil, errors.Errorf("application offer URL has invalid form, must be [<user/]<model>.<appname>: %q", urlStr) 136 } 137 if !allowIncomplete && result.ModelName == "" { 138 return nil, errors.Errorf("application offer URL is missing model") 139 } 140 if !allowIncomplete && result.ApplicationName == "" { 141 return nil, errors.Errorf("application offer URL is missing application") 142 } 143 144 // Application name part may contain a relation name part, so strip that bit out 145 // before validating the name. 146 appName := strings.Split(result.ApplicationName, ":")[0] 147 // Validate the resulting URL part values. 148 if result.User != "" && !names.IsValidUser(result.User) { 149 return nil, errors.NotValidf("user name %q", result.User) 150 } 151 if result.ModelName != "" && !names.IsValidModelName(result.ModelName) { 152 return nil, errors.NotValidf("model name %q", result.ModelName) 153 } 154 if appName != "" && !names.IsValidApplication(appName) { 155 return nil, errors.NotValidf("application name %q", appName) 156 } 157 return &result, nil 158 } 159 160 // MakeURL constructs an offer URL from the specified components. 161 func MakeURL(user, model, application, controller string) string { 162 base := fmt.Sprintf("%s/%s.%s", user, model, application) 163 if controller == "" { 164 return base 165 } 166 return fmt.Sprintf("%s:%s", controller, base) 167 }