From d21c11d774df552b9c36d3d35aa8eb12e5aebd55 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Fri, 30 Jun 2023 20:20:53 +0900 Subject: [PATCH] Add tenancy support (#7636) --- internal/ghinstance/host.go | 41 +++++++++++--- internal/ghinstance/host_test.go | 94 ++++++++++++++++++++++++++++++++ internal/ghrepo/repo.go | 5 +- internal/ghrepo/repo_test.go | 56 +++++++++++++++++++ 4 files changed, 186 insertions(+), 10 deletions(-) diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go index 1fde25e23..729d47f31 100644 --- a/internal/ghinstance/host.go +++ b/internal/ghinstance/host.go @@ -6,37 +6,56 @@ import ( "strings" ) +// DefaultHostname is the domain name of the default GitHub instance. const defaultHostname = "github.com" -// localhost is the domain name of a local GitHub instance +// Localhost is the domain name of a local GitHub instance. const localhost = "github.localhost" -// Default returns the host name of the default GitHub instance +// TenancyHost is the domain name of a tenancy GitHub instance. +const tenancyHost = "ghe.com" + +// Default returns the host name of the default GitHub instance. func Default() string { return defaultHostname } -// IsEnterprise reports whether a non-normalized host name looks like a GHE instance +// IsEnterprise reports whether a non-normalized host name looks like a GHE instance. func IsEnterprise(h string) bool { normalizedHostName := NormalizeHostname(h) return normalizedHostName != defaultHostname && normalizedHostName != localhost } +// IsTenancy reports whether a non-normalized host name looks like a tenancy instance. +func IsTenancy(h string) bool { + normalizedHostName := NormalizeHostname(h) + return strings.HasSuffix(normalizedHostName, "."+tenancyHost) +} + +// TenantName extracts the tenant name from tenancy host name and +// reports whether it found the tenant name. +func TenantName(h string) (string, bool) { + normalizedHostName := NormalizeHostname(h) + return cutSuffix(normalizedHostName, "."+tenancyHost) +} + func isGarage(h string) bool { return strings.EqualFold(h, "garage.github.com") } -// NormalizeHostname returns the canonical host name of a GitHub instance +// NormalizeHostname returns the canonical host name of a GitHub instance. func NormalizeHostname(h string) string { hostname := strings.ToLower(h) if strings.HasSuffix(hostname, "."+defaultHostname) { return defaultHostname } - if strings.HasSuffix(hostname, "."+localhost) { return localhost } - + if before, found := cutSuffix(hostname, "."+tenancyHost); found { + idx := strings.LastIndex(before, ".") + return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost) + } return hostname } @@ -78,11 +97,9 @@ func RESTPrefix(hostname string) string { func GistPrefix(hostname string) string { prefix := "https://" - if strings.EqualFold(hostname, localhost) { prefix = "http://" } - return prefix + GistHost(hostname) } @@ -105,3 +122,11 @@ func HostPrefix(hostname string) string { } return fmt.Sprintf("https://%s/", hostname) } + +// Backport strings.CutSuffix from Go 1.20. +func cutSuffix(s, suffix string) (string, bool) { + if !strings.HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} diff --git a/internal/ghinstance/host_test.go b/internal/ghinstance/host_test.go index 5fa6eb4f3..673fad24c 100644 --- a/internal/ghinstance/host_test.go +++ b/internal/ghinstance/host_test.go @@ -49,6 +49,87 @@ func TestIsEnterprise(t *testing.T) { } } +func TestIsTenancy(t *testing.T) { + tests := []struct { + host string + want bool + }{ + { + host: "github.com", + want: false, + }, + { + host: "github.localhost", + want: false, + }, + { + host: "garage.github.com", + want: false, + }, + { + host: "ghe.com", + want: false, + }, + { + host: "tenant.ghe.com", + want: true, + }, + { + host: "api.tenant.ghe.com", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if got := IsTenancy(tt.host); got != tt.want { + t.Errorf("IsTenancy() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTenantName(t *testing.T) { + tests := []struct { + host string + wantTenant string + wantFound bool + }{ + { + host: "github.com", + wantTenant: "github.com", + }, + { + host: "github.localhost", + wantTenant: "github.localhost", + }, + { + host: "garage.github.com", + wantTenant: "github.com", + }, + { + host: "ghe.com", + wantTenant: "ghe.com", + }, + { + host: "tenant.ghe.com", + wantTenant: "tenant", + wantFound: true, + }, + { + host: "api.tenant.ghe.com", + wantTenant: "tenant", + wantFound: true, + }, + } + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + if tenant, found := TenantName(tt.host); tenant != tt.wantTenant || found != tt.wantFound { + t.Errorf("TenantName(%v) = %v %v, want %v %v", tt.host, tenant, found, tt.wantTenant, tt.wantFound) + } + }) + } +} + func TestNormalizeHostname(t *testing.T) { tests := []struct { host string @@ -90,6 +171,18 @@ func TestNormalizeHostname(t *testing.T) { host: "git.my.org", want: "git.my.org", }, + { + host: "ghe.com", + want: "ghe.com", + }, + { + host: "tenant.ghe.com", + want: "tenant.ghe.com", + }, + { + host: "api.tenant.ghe.com", + want: "tenant.ghe.com", + }, } for _, tt := range tests { t.Run(tt.host, func(t *testing.T) { @@ -139,6 +232,7 @@ func TestHostnameValidator(t *testing.T) { }) } } + func TestGraphQLEndpoint(t *testing.T) { tests := []struct { host string diff --git a/internal/ghrepo/repo.go b/internal/ghrepo/repo.go index 9c6c48a4d..4f0328e99 100644 --- a/internal/ghrepo/repo.go +++ b/internal/ghrepo/repo.go @@ -92,12 +92,13 @@ func GenerateRepoURL(repo Interface, p string, args ...interface{}) string { return baseURL } -// TODO there is a parallel implementation for non-isolated commands func FormatRemoteURL(repo Interface, protocol string) string { if protocol == "ssh" { + if tenant, found := ghinstance.TenantName(repo.RepoHost()); found { + return fmt.Sprintf("%s@%s:%s/%s.git", tenant, repo.RepoHost(), repo.RepoOwner(), repo.RepoName()) + } return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName()) } - return fmt.Sprintf("%s%s/%s.git", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) } diff --git a/internal/ghrepo/repo_test.go b/internal/ghrepo/repo_test.go index 1530f8f24..7ea6b0598 100644 --- a/internal/ghrepo/repo_test.go +++ b/internal/ghrepo/repo_test.go @@ -220,3 +220,59 @@ func TestFromFullName(t *testing.T) { }) } } + +func TestFormatRemoteURL(t *testing.T) { + tests := []struct { + name string + repoHost string + repoOwner string + repoName string + protocol string + want string + }{ + { + name: "https protocol", + repoHost: "github.com", + repoOwner: "owner", + repoName: "name", + protocol: "https", + want: "https://github.com/owner/name.git", + }, + { + name: "https protocol local host", + repoHost: "github.localhost", + repoOwner: "owner", + repoName: "name", + protocol: "https", + want: "http://github.localhost/owner/name.git", + }, + { + name: "ssh protocol", + repoHost: "github.com", + repoOwner: "owner", + repoName: "name", + protocol: "ssh", + want: "git@github.com:owner/name.git", + }, + { + name: "ssh protocol tenancy host", + repoHost: "tenant.ghe.com", + repoOwner: "owner", + repoName: "name", + protocol: "ssh", + want: "tenant@tenant.ghe.com:owner/name.git", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := ghRepo{ + hostname: tt.repoHost, + owner: tt.repoOwner, + name: tt.repoName, + } + if url := FormatRemoteURL(r, tt.protocol); url != tt.want { + t.Errorf("expected url %q, got %q", tt.want, url) + } + }) + } +}