Inherit API endpoint configuration from the Codespaces environment (#4723)
gh codespace commands now respect the following environment variables: - GITHUB_SERVER_URL: typically "https://github.com" - GITHUB_API_URL: typically "https://api.github.com" - INTERNAL_VSCS_TARGET_URL: typically "https://online.visualstudio.com" - GITHUB_TOKEN when CODESPACES is set, even if the host connecting to isn't "github.com". This set of changes ensures that `gh codespace` commands automatically connect to the right API endpoints when gh is used within a codespace. Co-authored-by: Mislav Marohnić <mislav@github.com>
This commit is contained in:
parent
3aef78fb71
commit
a3940020f9
5 changed files with 79 additions and 23 deletions
|
|
@ -45,25 +45,40 @@ import (
|
|||
"github.com/opentracing/opentracing-go"
|
||||
)
|
||||
|
||||
const githubAPI = "https://api.github.com"
|
||||
const (
|
||||
githubServer = "https://github.com"
|
||||
githubAPI = "https://api.github.com"
|
||||
vscsAPI = "https://online.visualstudio.com"
|
||||
)
|
||||
|
||||
// API is the interface to the codespace service.
|
||||
type API struct {
|
||||
token string
|
||||
client httpClient
|
||||
githubAPI string
|
||||
client httpClient
|
||||
vscsAPI string
|
||||
githubAPI string
|
||||
githubServer string
|
||||
}
|
||||
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// New creates a new API client with the given token and HTTP client.
|
||||
func New(token string, httpClient httpClient) *API {
|
||||
// New creates a new API client connecting to the configured endpoints with the HTTP client.
|
||||
func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
|
||||
if serverURL == "" {
|
||||
serverURL = githubServer
|
||||
}
|
||||
if apiURL == "" {
|
||||
apiURL = githubAPI
|
||||
}
|
||||
if vscsURL == "" {
|
||||
vscsURL = vscsAPI
|
||||
}
|
||||
return &API{
|
||||
token: token,
|
||||
client: httpClient,
|
||||
githubAPI: githubAPI,
|
||||
client: httpClient,
|
||||
vscsAPI: strings.TrimSuffix(vscsURL, "/"),
|
||||
githubAPI: strings.TrimSuffix(apiURL, "/"),
|
||||
githubServer: strings.TrimSuffix(serverURL, "/"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +401,7 @@ type getCodespaceRegionLocationResponse struct {
|
|||
|
||||
// GetCodespaceRegionLocation returns the closest codespace location for the user.
|
||||
func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://online.visualstudio.com/api/v1/locations", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, a.vscsAPI+"/api/v1/locations", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
|
@ -635,7 +650,7 @@ func (a *API) GetCodespaceRepositoryContents(ctx context.Context, codespace *Cod
|
|||
// AuthorizedKeys returns the public keys (in ~/.ssh/authorized_keys
|
||||
// format) registered by the specified GitHub user.
|
||||
func (a *API) AuthorizedKeys(ctx context.Context, user string) ([]byte, error) {
|
||||
url := fmt.Sprintf("https://github.com/%s.keys", user)
|
||||
url := fmt.Sprintf("%s/%s.keys", a.githubServer, user)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -669,8 +684,5 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
|
|||
|
||||
// setHeaders sets the required headers for the API.
|
||||
func (a *API) setHeaders(req *http.Request) {
|
||||
if a.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+a.token)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ func TestListCodespaces_limited(t *testing.T) {
|
|||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
token: "faketoken",
|
||||
}
|
||||
ctx := context.TODO()
|
||||
codespaces, err := api.ListCodespaces(ctx, 200)
|
||||
|
|
@ -98,7 +97,6 @@ func TestListCodespaces_unlimited(t *testing.T) {
|
|||
api := API{
|
||||
githubAPI: svr.URL,
|
||||
client: &http.Client{},
|
||||
token: "faketoken",
|
||||
}
|
||||
ctx := context.TODO()
|
||||
codespaces, err := api.ListCodespaces(ctx, -1)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
)
|
||||
|
|
@ -13,6 +14,7 @@ const (
|
|||
GITHUB_TOKEN = "GITHUB_TOKEN"
|
||||
GH_ENTERPRISE_TOKEN = "GH_ENTERPRISE_TOKEN"
|
||||
GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN"
|
||||
CODESPACES = "CODESPACES"
|
||||
)
|
||||
|
||||
type ReadOnlyEnvError struct {
|
||||
|
|
@ -90,7 +92,15 @@ func AuthTokenFromEnv(hostname string) (string, string) {
|
|||
return token, GH_ENTERPRISE_TOKEN
|
||||
}
|
||||
|
||||
return os.Getenv(GITHUB_ENTERPRISE_TOKEN), GITHUB_ENTERPRISE_TOKEN
|
||||
if token := os.Getenv(GITHUB_ENTERPRISE_TOKEN); token != "" {
|
||||
return token, GITHUB_ENTERPRISE_TOKEN
|
||||
}
|
||||
|
||||
if isCodespaces, _ := strconv.ParseBool(os.Getenv(CODESPACES)); isCodespaces {
|
||||
return os.Getenv(GITHUB_TOKEN), GITHUB_TOKEN
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if token := os.Getenv(GH_TOKEN); token != "" {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,18 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setenv(t *testing.T, key, newValue string) {
|
||||
oldValue, hasValue := os.LookupEnv(key)
|
||||
os.Setenv(key, newValue)
|
||||
t.Cleanup(func() {
|
||||
if hasValue {
|
||||
os.Setenv(key, oldValue)
|
||||
} else {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInheritEnv(t *testing.T) {
|
||||
orig_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN")
|
||||
orig_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
|
||||
|
|
@ -36,6 +48,7 @@ func TestInheritEnv(t *testing.T) {
|
|||
GITHUB_ENTERPRISE_TOKEN string
|
||||
GH_TOKEN string
|
||||
GH_ENTERPRISE_TOKEN string
|
||||
CODESPACES string
|
||||
hostname string
|
||||
wants wants
|
||||
}{
|
||||
|
|
@ -98,6 +111,19 @@ func TestInheritEnv(t *testing.T) {
|
|||
writeable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GITHUB_TOKEN allowed in Codespaces",
|
||||
baseConfig: ``,
|
||||
GITHUB_TOKEN: "OTOKEN",
|
||||
hostname: "example.org",
|
||||
CODESPACES: "true",
|
||||
wants: wants{
|
||||
hosts: []string{"github.com"},
|
||||
token: "OTOKEN",
|
||||
source: "GITHUB_TOKEN",
|
||||
writeable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GITHUB_ENTERPRISE_TOKEN over blank config",
|
||||
baseConfig: ``,
|
||||
|
|
@ -262,11 +288,12 @@ func TestInheritEnv(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv("GITHUB_TOKEN", tt.GITHUB_TOKEN)
|
||||
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
|
||||
os.Setenv("GH_TOKEN", tt.GH_TOKEN)
|
||||
os.Setenv("GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
|
||||
os.Setenv("AppData", "")
|
||||
setenv(t, "GITHUB_TOKEN", tt.GITHUB_TOKEN)
|
||||
setenv(t, "GITHUB_ENTERPRISE_TOKEN", tt.GITHUB_ENTERPRISE_TOKEN)
|
||||
setenv(t, "GH_TOKEN", tt.GH_TOKEN)
|
||||
setenv(t, "GH_ENTERPRISE_TOKEN", tt.GH_ENTERPRISE_TOKEN)
|
||||
setenv(t, "AppData", "")
|
||||
setenv(t, "CODESPACES", tt.CODESPACES)
|
||||
|
||||
baseCfg := NewFromString(tt.baseConfig)
|
||||
cfg := InheritEnv(baseCfg)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package root
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -129,9 +130,17 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er
|
|||
}
|
||||
|
||||
func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command {
|
||||
serverURL := os.Getenv("GITHUB_SERVER_URL")
|
||||
apiURL := os.Getenv("GITHUB_API_URL")
|
||||
vscsURL := os.Getenv("INTERNAL_VSCS_TARGET_URL")
|
||||
app := codespaceCmd.NewApp(
|
||||
f.IOStreams,
|
||||
codespacesAPI.New("", &lazyLoadedHTTPClient{factory: f}),
|
||||
codespacesAPI.New(
|
||||
serverURL,
|
||||
apiURL,
|
||||
vscsURL,
|
||||
&lazyLoadedHTTPClient{factory: f},
|
||||
),
|
||||
)
|
||||
cmd := codespaceCmd.NewRootCmd(app)
|
||||
cmd.Use = "codespace"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue