diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 41fa4cc3f..8de16bbb8 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -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") } diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index e8748fa9a..6dcd06a04 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -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) diff --git a/internal/config/from_env.go b/internal/config/from_env.go index 6373f1691..ad31537f4 100644 --- a/internal/config/from_env.go +++ b/internal/config/from_env.go @@ -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 != "" { diff --git a/internal/config/from_env_test.go b/internal/config/from_env_test.go index 765cd0160..bf81c7976 100644 --- a/internal/config/from_env_test.go +++ b/internal/config/from_env_test.go @@ -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) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 7a1e08674..d393d1a7f 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -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"