Provisioning: Fix customServerURL resolution for GHE (#127113)

* Provisioning: Fix customServerURL resolution for GHE
This commit is contained in:
Florie Cai
2026-06-25 18:58:01 -04:00
committed by GitHub
parent dd857cc660
commit 013ce244b8
5 changed files with 113 additions and 29 deletions
@@ -31,7 +31,7 @@ type ClientOptions struct {
// ClientOption customizes how a GitHub client is created.
type ClientOption func(*ClientOptions)
// WithCustomServerURL targets a GitHub Enterprise Server instance at the given
// WithCustomServerURL targets a GitHub Enterprise instance at the given REST API
// base URL. An empty url is ignored, keeping the default github.com client.
func WithCustomServerURL(url string) ClientOption {
return func(o *ClientOptions) {
@@ -46,7 +46,15 @@ func (r *Factory) New(ctx context.Context, ghToken common.RawSecureValue, opts .
}
if r.Client != nil {
return NewClient(github.NewClient(r.Client)), nil
ghClient := github.NewClient(r.Client)
if options.customServerURL != "" {
enterprise, err := ghClient.WithEnterpriseURLs(options.customServerURL, options.customServerURL)
if err != nil {
return nil, fmt.Errorf("failed to configure GitHub Enterprise URLs for %q: %w", options.customServerURL, err)
}
ghClient = enterprise
}
return NewClient(ghClient), nil
}
httpClient := &http.Client{}
@@ -0,0 +1,49 @@
package github
import (
"context"
"testing"
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// TestFactoryNew_EnterpriseBaseURL verifies that a resolved REST API base URL (as produced by
// the githubenterprise config's CustomServerURL) maps to the expected go-github BaseURL. The
// web-URL -> API-URL resolution per deployment type is tested where it lives, in that config.
func TestFactoryNew_EnterpriseBaseURL(t *testing.T) {
tests := []struct {
name string
serverURL string
expected string
}{
{
name: "no custom server url uses default api.github.com",
serverURL: "",
expected: "https://api.github.com/",
},
{
name: "GHES self-hosted appends /api/v3",
serverURL: "https://custom-ghe-url.com",
expected: "https://custom-ghe-url.com/api/v3/",
},
{
name: "GHEC data residency api host stays without /api/v3",
serverURL: "https://api.acme.ghe.com",
expected: "https://api.acme.ghe.com/",
},
}
factory := &Factory{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := factory.New(context.Background(), common.RawSecureValue(""), WithCustomServerURL(tt.serverURL))
require.NoError(t, err)
gc, ok := c.(*githubClient)
require.True(t, ok, "expected *githubClient")
require.Equal(t, tt.expected, gc.gh.BaseURL.String())
})
}
}
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/google/go-github/v82/github"
"golang.org/x/oauth2"
@@ -39,20 +40,34 @@ type ClientOptions struct {
// ClientOption customizes how a GitHub client is created.
type ClientOption func(*ClientOptions)
// WithCustomServerURL targets a GitHub Enterprise Server instance at the given
// base URL. An empty url or the default `https://github.com` is ignored, keeping the default github.com client.
// WithCustomServerURL resolves a GitHub repo/web URL into the REST API base URL for its
// deployment type and targets the client at it. An empty url or github.com keeps the default
// github.com client.
//
// GHES (self-hosted): https://custom-ghe-url.com/owner/repo -> https://custom-ghe-url.com (go-github appends /api/v3)
// GHEC data residency: https://acme.ghe.com/owner/repo -> https://api.acme.ghe.com (REST API has no /api/v3 prefix)
// GHEC standard / EMU: https://github.com/owner/repo -> "" (default api.github.com client)
//
// The api. prefix on data-residency hosts is also what tells go-github's WithEnterpriseURLs to
// skip appending the /api/v3 path that self-hosted servers require, so the two cases diverge.
func WithCustomServerURL(serverURL string) ClientOption {
return func(o *ClientOptions) {
u, err := url.Parse(serverURL)
if err != nil {
if err != nil || u.Host == "" {
return
}
if u.Host == "" || u.Host == "github.com" {
host := u.Hostname()
if host == "github.com" || host == "www.github.com" {
return
}
o.customServerURL = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
if strings.HasSuffix(host, ".ghe.com") && !strings.HasPrefix(host, "api.") {
o.customServerURL = u.Scheme + "://api." + u.Host
return
}
o.customServerURL = u.Scheme + "://" + u.Host
}
}
@@ -1,44 +1,54 @@
package github
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
"github.com/grafana/grafana/apps/provisioning/pkg/repository/git"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
func TestWithCustomServerURL(t *testing.T) {
// TestNewRepository_ServerURL verifies the REST API base URL on the underlying client is resolved
// from the repo URL's host per GitHub deployment type.
func TestNewRepository_ServerURL(t *testing.T) {
tests := []struct {
name string
serverURL string
expected string
name string
repoURL string
expected string
}{
{
name: "default github.com is ignored",
serverURL: "https://github.com",
expected: "",
name: "plain github.com uses default api.github.com",
repoURL: "https://github.com/grafana/grafana",
expected: "https://api.github.com/",
},
{
name: "github.com with path is ignored",
serverURL: "https://github.com/example/test",
expected: "",
name: "GHES appends /api/v3",
repoURL: "https://custom-ghe-url.com/owner/repo",
expected: "https://custom-ghe-url.com/api/v3/",
},
{
name: "empty url is ignored",
serverURL: "",
expected: "",
},
{
name: "GitHub Enterprise Server url is kept",
serverURL: "https://ghes.example.com/example/test",
expected: "https://ghes.example.com",
name: "data residency rewrites to api host without /api/v3",
repoURL: "https://acme.ghe.com/owner/repo",
expected: "https://api.acme.ghe.com/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var options ClientOptions
WithCustomServerURL(tt.serverURL)(&options)
assert.Equal(t, tt.expected, options.customServerURL)
gitRepo := git.NewMockGitRepository(t)
gitRepo.EXPECT().URL().Return(tt.repoURL).Maybe()
repo, err := NewRepository(context.Background(), &provisioning.Repository{}, gitRepo, ProvideFactory(), common.RawSecureValue(""))
require.NoError(t, err)
gr, ok := repo.(*githubRepository)
require.True(t, ok, "expected *githubRepository")
gc, ok := gr.gh.(*githubClient)
require.True(t, ok, "expected *githubClient")
require.Equal(t, tt.expected, gc.gh.BaseURL.String())
})
}
}
@@ -7,6 +7,7 @@ package extensions
import (
_ "github.com/alicebob/miniredis/v2"
_ "github.com/google/go-github/v82/github"
_ "github.com/grafana/authlib/authz/proto/v1"
_ "github.com/grafana/dataplane/examples"
_ "github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
@@ -48,6 +49,7 @@ import (
_ "github.com/grafana/grafana/pkg/tests/testsuite"
_ "github.com/grafana/grafana/pkg/util/testutil"
_ "github.com/grafana/grafana/pkg/web/webtest"
_ "github.com/migueleliasweb/go-github-mock/src/mock"
_ "github.com/open-feature/go-sdk/openfeature/testing"
_ "github.com/openfga/openfga/pkg/server/test"
_ "github.com/openfga/openfga/pkg/storage/test"