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  }