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:
Marwan Sulaiman 2021-11-22 06:40:01 -05:00 committed by GitHub
parent 3aef78fb71
commit a3940020f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 23 deletions

View file

@ -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")
}

View file

@ -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)

View file

@ -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 != "" {

View file

@ -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)

View file

@ -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"