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 }