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