github.com/fluxcd/go-git-providers@v0.19.3/gitprovider/repositoryref.go (about)

     1  /*
     2  Copyright 2020 The Flux CD contributors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package gitprovider
    18  
    19  import (
    20  	"fmt"
    21  	"net/url"
    22  	"strings"
    23  
    24  	"github.com/fluxcd/go-git-providers/validation"
    25  )
    26  
    27  // IdentityType is a typed string for what kind of identity type an IdentityRef is.
    28  type IdentityType string
    29  
    30  const (
    31  	// IdentityTypeUser represents an identity for a user account.
    32  	IdentityTypeUser = IdentityType("user")
    33  	// IdentityTypeOrganization represents an identity for an organization.
    34  	IdentityTypeOrganization = IdentityType("organization")
    35  	// IdentityTypeSuborganization represents an identity for a sub-organization.
    36  	IdentityTypeSuborganization = IdentityType("suborganization")
    37  )
    38  
    39  // IdentityRef references an organization or user account in a Git provider.
    40  type IdentityRef interface {
    41  	// IdentityRef implements ValidateTarget so it can easily be validated as a field.
    42  	validation.ValidateTarget
    43  
    44  	// GetDomain returns the URL-domain for the Git provider backend,
    45  	// e.g. "github.com" or "self-hosted-gitlab.com:6443".
    46  	GetDomain() string
    47  
    48  	// GetIdentity returns the user account name or a slash-separated path of the
    49  	// <organization-name>[/<sub-organization-name>...] form. This can be used as
    50  	// an identifier for this specific actor in the system.
    51  	GetIdentity() string
    52  
    53  	// GetType returns what type of identity this instance represents. If IdentityTypeUser is returned
    54  	// this IdentityRef can safely be casted to an UserRef. If any of IdentityTypeOrganization or
    55  	// IdentityTypeSuborganization are returned, this IdentityRef can be casted to a OrganizationRef.
    56  	GetType() IdentityType
    57  
    58  	// String returns the URL, and implements fmt.Stringer.
    59  	String() string
    60  }
    61  
    62  // Keyer is an interface that can be used to get a unique key for an object.
    63  type Keyer interface {
    64  	// Key returns a unique key for this object.
    65  	Key() string
    66  }
    67  
    68  // RepositoryRef describes a reference to a repository owned by either a user account or organization.
    69  type RepositoryRef interface {
    70  	// RepositoryRef is a superset of IdentityRef.
    71  	IdentityRef
    72  
    73  	// GetRepository returns the repository name for this repo.
    74  	GetRepository() string
    75  
    76  	// GetCloneURL gets the clone URL for the specified transport type.
    77  	GetCloneURL(transport TransportType) string
    78  }
    79  
    80  // Slugger is an interface that can be used to get a unique slug for an object.
    81  type Slugger interface {
    82  	// Slug returns the unique slug for this object.
    83  	Slug() string
    84  }
    85  
    86  // UserRef represents a user account in a Git provider.
    87  type UserRef struct {
    88  	// Domain returns e.g. "github.com", "gitlab.com" or a custom domain like "self-hosted-gitlab.com" (GitLab)
    89  	// The domain _might_ contain port information, in the form of "host:port", if applicable
    90  	// +required
    91  	Domain string `json:"domain"`
    92  
    93  	// UserLogin returns the user account login name.
    94  	// +required
    95  	UserLogin string `json:"userLogin"`
    96  }
    97  
    98  // UserRef implements IdentityRef.
    99  var _ IdentityRef = UserRef{}
   100  
   101  // GetDomain returns the the domain part of the endpoint, can include port information.
   102  func (u UserRef) GetDomain() string {
   103  	return u.Domain
   104  }
   105  
   106  // GetIdentity returns the identity of this actor, which in this case is the user login name.
   107  func (u UserRef) GetIdentity() string {
   108  	return u.UserLogin
   109  }
   110  
   111  // GetType marks this UserRef as being a IdentityTypeUser.
   112  func (u UserRef) GetType() IdentityType {
   113  	return IdentityTypeUser
   114  }
   115  
   116  // String returns the HTTPS URL to access the User.
   117  func (u UserRef) String() string {
   118  	domain := GetDomainURL(u.GetDomain())
   119  	return fmt.Sprintf("%s/%s", domain, u.GetIdentity())
   120  }
   121  
   122  // ValidateFields validates its own fields for a given validator.
   123  func (u UserRef) ValidateFields(validator validation.Validator) {
   124  	// Require the Domain and Organization to be set
   125  	if len(u.Domain) == 0 {
   126  		validator.Required("Domain")
   127  	}
   128  	if len(u.UserLogin) == 0 {
   129  		validator.Required("UserLogin")
   130  	}
   131  }
   132  
   133  // OrganizationRef implements IdentityRef.
   134  var _ IdentityRef = OrganizationRef{}
   135  
   136  // OrganizationRef is an implementation of OrganizationRef.
   137  type OrganizationRef struct {
   138  	// Domain returns e.g. "github.com", "gitlab.com" or a custom domain like "self-hosted-gitlab.com" (GitLab)
   139  	// The domain _might_ contain port information, in the form of "host:port", if applicable.
   140  	// +required
   141  	Domain string `json:"domain"`
   142  
   143  	// Organization specifies the URL-friendly, lowercase name of the organization or user account name,
   144  	// e.g. "fluxcd" or "kubernetes-sigs".
   145  	// +required
   146  	Organization string `json:"organization"`
   147  
   148  	// key specifies the URL-friendly, lowercase key of the organization,
   149  	// e.g. "fluxcd" or "kubernetes-sigs".
   150  	// +optional
   151  	key string
   152  
   153  	// SubOrganizations point to optional sub-organizations (or sub-groups) of the given top-level organization
   154  	// in the Organization field. E.g. "gitlab.com/fluxcd/engineering/frontend" would yield ["engineering", "frontend"]
   155  	// +optional
   156  	SubOrganizations []string `json:"subOrganizations,omitempty"`
   157  }
   158  
   159  // GetDomain returns the the domain part of the endpoint, can include port information.
   160  func (o OrganizationRef) GetDomain() string {
   161  	return o.Domain
   162  }
   163  
   164  // GetIdentity returns the identity of this actor, which in this case is the user login name.
   165  func (o OrganizationRef) GetIdentity() string {
   166  	orgParts := append([]string{o.Organization}, o.SubOrganizations...)
   167  	return strings.Join(orgParts, "/")
   168  }
   169  
   170  // Key returns the unique key for this OrganizationRef.
   171  func (o OrganizationRef) Key() string {
   172  	return o.key
   173  }
   174  
   175  // SetKey sets the unique key for this OrganizationRef.
   176  func (o *OrganizationRef) SetKey(key string) {
   177  	o.key = key
   178  }
   179  
   180  // GetType marks this UserRef as being a IdentityTypeUser.
   181  func (o OrganizationRef) GetType() IdentityType {
   182  	if len(o.SubOrganizations) > 0 {
   183  		return IdentityTypeSuborganization
   184  	}
   185  	return IdentityTypeOrganization
   186  }
   187  
   188  // String returns the URL to access the Organization.
   189  func (o OrganizationRef) String() string {
   190  	domain := GetDomainURL(o.GetDomain())
   191  	return fmt.Sprintf("%s/%s", domain, o.GetIdentity())
   192  }
   193  
   194  // ValidateFields validates its own fields for a given validator.
   195  func (o OrganizationRef) ValidateFields(validator validation.Validator) {
   196  	// Require the Domain and Organization to be set
   197  	if len(o.Domain) == 0 {
   198  		validator.Required("Domain")
   199  	}
   200  	if len(o.Organization) == 0 {
   201  		validator.Required("Organization")
   202  	}
   203  }
   204  
   205  // OrgRepositoryRef is a struct with information about a specific repository owned by an organization.
   206  type OrgRepositoryRef struct {
   207  	// OrgRepositoryRef embeds OrganizationRef inline.
   208  	OrganizationRef `json:",inline"`
   209  
   210  	// RepositoryName specifies the Git repository name. This field is URL-friendly,
   211  	// e.g. "kubernetes" or "cluster-api-provider-aws".
   212  	// +required
   213  	RepositoryName string `json:"repositoryName"`
   214  
   215  	// slug specifies the Git repository slug. This field is URL-friendly,
   216  	// e.g. "kubernetes" or "cluster-api-provider-aws".
   217  	// +optional
   218  	slug string
   219  }
   220  
   221  // String returns the HTTPS URL to access the repository.
   222  func (r OrgRepositoryRef) String() string {
   223  	return fmt.Sprintf("%s/%s", r.OrganizationRef.String(), r.RepositoryName)
   224  }
   225  
   226  // GetRepository returns the repository name for this repo.
   227  func (r OrgRepositoryRef) GetRepository() string {
   228  	return r.RepositoryName
   229  }
   230  
   231  // Slug returns the unique slug for this object.
   232  func (r OrgRepositoryRef) Slug() string {
   233  	return r.slug
   234  }
   235  
   236  // SetSlug sets the unique slug for this object.
   237  func (r *OrgRepositoryRef) SetSlug(slug string) {
   238  	r.slug = slug
   239  }
   240  
   241  // ValidateFields validates its own fields for a given validator.
   242  func (r OrgRepositoryRef) ValidateFields(validator validation.Validator) {
   243  	// First, validate the embedded OrganizationRef
   244  	r.OrganizationRef.ValidateFields(validator)
   245  	// Require RepositoryName to be set
   246  	if len(r.RepositoryName) == 0 {
   247  		validator.Required("RepositoryName")
   248  	}
   249  }
   250  
   251  // GetCloneURL gets the clone URL for the specified transport type.
   252  func (r OrgRepositoryRef) GetCloneURL(transport TransportType) string {
   253  	return GetCloneURL(r, transport)
   254  }
   255  
   256  // UserRepositoryRef is a struct with information about a specific repository owned by a user.
   257  type UserRepositoryRef struct {
   258  	// UserRepositoryRef embeds UserRef inline.
   259  	UserRef `json:",inline"`
   260  
   261  	// RepositoryName specifies the Git repository name. This field is URL-friendly,
   262  	// e.g. "kubernetes" or "cluster-api-provider-aws".
   263  	// +required
   264  	RepositoryName string `json:"repositoryName"`
   265  
   266  	// slug specifies the Git repository slug. This field is URL-friendly,
   267  	// e.g. "kubernetes" or "cluster-api-provider-aws".
   268  	// +optional
   269  	slug string
   270  }
   271  
   272  // String returns the URL to access the repository.
   273  func (r UserRepositoryRef) String() string {
   274  	return fmt.Sprintf("%s/%s", r.UserRef.String(), r.RepositoryName)
   275  }
   276  
   277  // GetRepository returns the repository name for this repo.
   278  func (r UserRepositoryRef) GetRepository() string {
   279  	return r.RepositoryName
   280  }
   281  
   282  // Slug returns the unique slug for this object.
   283  func (r UserRepositoryRef) Slug() string {
   284  	return r.slug
   285  }
   286  
   287  // SetSlug sets the unique slug for this object.
   288  func (r *UserRepositoryRef) SetSlug(slug string) {
   289  	r.slug = slug
   290  }
   291  
   292  // ValidateFields validates its own fields for a given validator.
   293  func (r UserRepositoryRef) ValidateFields(validator validation.Validator) {
   294  	// First, validate the embedded OrganizationRef
   295  	r.UserRef.ValidateFields(validator)
   296  	// Require RepositoryName to be set
   297  	if len(r.RepositoryName) == 0 {
   298  		validator.Required("RepositoryName")
   299  	}
   300  }
   301  
   302  // GetCloneURL gets the clone URL for the specified transport type.
   303  func (r UserRepositoryRef) GetCloneURL(transport TransportType) string {
   304  	return GetCloneURL(r, transport)
   305  }
   306  
   307  // GetCloneURL returns the URL to clone a repository for a given transport type. If the given
   308  // TransportType isn't known an empty string is returned.
   309  func GetCloneURL(rs RepositoryRef, transport TransportType) string {
   310  	switch transport {
   311  	case TransportTypeHTTPS:
   312  		return ParseTypeHTTPS(rs.String())
   313  	case TransportTypeGit:
   314  		return ParseTypeGit(rs.GetDomain(), rs.GetIdentity(), rs.GetRepository())
   315  	case TransportTypeSSH:
   316  		return ParseTypeSSH(rs.GetDomain(), rs.GetIdentity(), rs.GetRepository())
   317  	}
   318  	return ""
   319  }
   320  
   321  // ParseTypeHTTPS returns the HTTPS URL to clone a repository.
   322  func ParseTypeHTTPS(url string) string {
   323  	return fmt.Sprintf("%s.git", url)
   324  }
   325  
   326  // ParseTypeGit returns the URL to clone a repository using the Git protocol.
   327  func ParseTypeGit(domain, identity, repository string) string {
   328  	return fmt.Sprintf("git@%s:%s/%s.git", domain, identity, repository)
   329  }
   330  
   331  // ParseTypeSSH returns the URL to clone a repository using the SSH protocol.
   332  func ParseTypeSSH(domain, identity, repository string) string {
   333  	trimmedDomain := domain
   334  	trimmedDomain = strings.Replace(trimmedDomain, "https://", "", -1)
   335  	trimmedDomain = strings.Replace(trimmedDomain, "http://", "", -1)
   336  	return fmt.Sprintf("ssh://git@%s/%s/%s", trimmedDomain, identity, repository)
   337  }
   338  
   339  // ParseOrganizationURL parses an URL to an organization into a OrganizationRef object.
   340  func ParseOrganizationURL(o string) (*OrganizationRef, error) {
   341  	u, parts, err := parseURL(o)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	// Create the IdentityInfo object
   346  	info := &OrganizationRef{
   347  		Domain:           u.Host,
   348  		Organization:     parts[0],
   349  		SubOrganizations: []string{},
   350  	}
   351  	// If we've got more than one part, assume they are sub-organizations
   352  	if len(parts) > 1 {
   353  		info.SubOrganizations = parts[1:]
   354  	}
   355  	return info, nil
   356  }
   357  
   358  // ParseUserURL parses an URL to an organization into a UserRef object.
   359  func ParseUserURL(u string) (*UserRef, error) {
   360  	// Use the same logic as for parsing organization URLs, but return an UserRef object
   361  	orgInfoPtr, err := ParseOrganizationURL(u)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	userRef, err := orgInfoPtrToUserRef(orgInfoPtr)
   366  	if err != nil {
   367  		return nil, fmt.Errorf("%w: %s", err, u)
   368  	}
   369  	return userRef, nil
   370  }
   371  
   372  // ParseUserRepositoryURL parses a HTTPS clone URL into a UserRepositoryRef object.
   373  func ParseUserRepositoryURL(r string) (*UserRepositoryRef, error) {
   374  	orgInfoPtr, repoName, err := parseRepositoryURL(r)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  
   379  	userRef, err := orgInfoPtrToUserRef(orgInfoPtr)
   380  	if err != nil {
   381  		return nil, fmt.Errorf("%w: %s", ErrURLInvalid, r)
   382  	}
   383  
   384  	return &UserRepositoryRef{
   385  		UserRef:        *userRef,
   386  		RepositoryName: repoName,
   387  	}, nil
   388  }
   389  
   390  // ParseOrgRepositoryURL parses a HTTPS clone URL into a OrgRepositoryRef object.
   391  func ParseOrgRepositoryURL(r string) (*OrgRepositoryRef, error) {
   392  	orgInfoPtr, repoName, err := parseRepositoryURL(r)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	return &OrgRepositoryRef{
   398  		OrganizationRef: *orgInfoPtr,
   399  		RepositoryName:  repoName,
   400  	}, nil
   401  }
   402  
   403  func parseRepositoryURL(r string) (orgInfoPtr *OrganizationRef, repoName string, err error) {
   404  	// First, parse the URL as an organization
   405  	orgInfoPtr, err = ParseOrganizationURL(r)
   406  	if err != nil {
   407  		return nil, "", err
   408  	}
   409  	// The "repository" part of the URL parsed as an organization, is the last "sub-organization"
   410  	// Check that there's at least one sub-organization
   411  	if len(orgInfoPtr.SubOrganizations) < 1 {
   412  		return nil, "", fmt.Errorf("%w: %s", ErrURLMissingRepoName, r)
   413  	}
   414  
   415  	// The repository name is the last "sub-org"
   416  	repoName = orgInfoPtr.SubOrganizations[len(orgInfoPtr.SubOrganizations)-1]
   417  	// Never include any .git suffix at the end of the repository name
   418  	repoName = strings.TrimSuffix(repoName, ".git")
   419  
   420  	// Remove the repository name from the sub-org list
   421  	orgInfoPtr.SubOrganizations = orgInfoPtr.SubOrganizations[:len(orgInfoPtr.SubOrganizations)-1]
   422  	return
   423  }
   424  
   425  func parseURL(str string) (*url.URL, []string, error) {
   426  	// Fail-fast if the URL is empty
   427  	if len(str) == 0 {
   428  		return nil, nil, fmt.Errorf("url cannot be empty: %w", ErrURLInvalid)
   429  	}
   430  	u, err := url.Parse(str)
   431  	if err != nil {
   432  		return nil, nil, err
   433  	}
   434  	// Only allow explicit https URLs
   435  	if u.Scheme != "https" {
   436  		return nil, nil, fmt.Errorf("%w: %s", ErrURLUnsupportedScheme, str)
   437  	}
   438  	// Don't allow any extra things in the URL, in order to be able to do a successful
   439  	// round-trip of parsing the URL and encoding it back to a string
   440  	if len(u.Fragment) != 0 || len(u.RawQuery) != 0 || len(u.User.String()) != 0 {
   441  		return nil, nil, fmt.Errorf("%w: %s", ErrURLUnsupportedParts, str)
   442  	}
   443  
   444  	// Strip any leading and trailing slash to be able to split the string cleanly
   445  	path := strings.TrimSuffix(strings.TrimPrefix(u.Path, "/"), "/")
   446  	// Split the path by slash
   447  	parts := strings.Split(path, "/")
   448  	// Make sure there aren't any "empty" string splits
   449  	// This has the consequence that it's guaranteed that there is at least one
   450  	// part returned, so there's no need to check for len(parts) < 1
   451  	for _, p := range parts {
   452  		// Make sure any path part is not empty
   453  		if len(p) == 0 {
   454  			return nil, nil, fmt.Errorf("%w: %s", ErrURLInvalid, str)
   455  		}
   456  	}
   457  	return u, parts, nil
   458  }
   459  
   460  func orgInfoPtrToUserRef(orgInfoPtr *OrganizationRef) (*UserRef, error) {
   461  	// Don't tolerate that there are "sub-parts" for an user URL
   462  	if len(orgInfoPtr.SubOrganizations) > 0 {
   463  		return nil, ErrURLInvalid
   464  	}
   465  	// Return an UserRef struct
   466  	return &UserRef{
   467  		Domain:    orgInfoPtr.Domain,
   468  		UserLogin: orgInfoPtr.Organization,
   469  	}, nil
   470  }