mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 13:51:46 +00:00
Migrates Cody Gateway to use the new Enterprise Portal's "read-only" APIs. For the most part, this is an in-place replacement - a lot of the diff is in testing and minor changes. Some changes, such as the removal of model allowlists, were made down the PR stack in https://github.com/sourcegraph/sourcegraph/pull/62911. At a high level, we replace the data requested by `cmd/cody-gateway/internal/dotcom/operations.graphql` and replace it with Enterprise Portal RPCs: - `codyaccessv1.GetCodyGatewayAccess` - `codyaccessv1.ListCodyGatewayAccesses` Use cases that previously required retrieving the active license tags now: 1. Use the display name provided by the Cody Access API https://github.com/sourcegraph/sourcegraph/pull/62968 2. Depend on the connected Enterprise Portal dev instance to only return dev subscriptions https://github.com/sourcegraph/sourcegraph/pull/62966 Closes https://linear.app/sourcegraph/issue/CORE-98 Related to https://linear.app/sourcegraph/issue/CORE-135 (https://github.com/sourcegraph/sourcegraph/pull/62909, https://github.com/sourcegraph/sourcegraph/pull/62911) Related to https://linear.app/sourcegraph/issue/CORE-97 ## Local development This change also adds Enterprise Portal to `sg start dotcom`. For local development, we set up Cody Gateway to connect to Enterprise Portal such that zero configuration is needed - all the required secrets are sourced from the `sourcegrah-local-dev` GCP project automatically when you run `sg start dotcom`, and local Cody Gateway will talk to local Enterprise Portal to do the Enterprise subscriptions sync. This is actually an upgrade from the current experience where you need to provide Cody Gateway a Sourcegraph user access token to test Enterprise locally, though the Sourcegraph user access token is still required for the PLG actor source. The credential is configured in https://console.cloud.google.com/security/secret-manager/secret/SG_LOCAL_DEV_SAMS_CLIENT_SECRET/overview?project=sourcegraph-local-dev, and I've included documentation in the secret annotation about what it is for and what to do with it:  ## Rollout plan I will open PRs to set up the necessary configuration for Cody Gateway dev and prod. Once reviews taper down I'll cut an image from this branch and deploy it to Cody Gateway dev, and monitor it closely + do some manual testing. Once verified, I'll land this change and monitor a rollout to production. Cody Gateway dev SAMS client: https://github.com/sourcegraph/infrastructure/pull/6108 Cody Gateway prod SAMS client update (this one already exists): ``` accounts=> UPDATE idp_clients SET scopes = scopes || '["enterprise_portal::subscription::read", "enterprise_portal::codyaccess::read"]'::jsonb WHERE id = 'sams_cid_018ea062-479e-7342-9473-66645e616cbf'; UPDATE 1 accounts=> select name, scopes from idp_clients WHERE name = 'Cody Gateway (prod)'; name | scopes ---------------------+---------------------------------------------------------------------------------------------------------------------------------- Cody Gateway (prod) | ["openid", "profile", "email", "offline_access", "enterprise_portal::subscription::read", "enterprise_portal::codyaccess::read"] (1 row) ``` Configuring the target Enterprise Portal instances: https://github.com/sourcegraph/infrastructure/pull/6127 ## Test plan Start the new `dotcom` runset, now including Enterprise Portal, and observe logs from both `enterprise-portal` and `cody-gateway`: ``` sg start dotcom ``` I reused the test plan from https://github.com/sourcegraph/sourcegraph/pull/62911: set up Cody Gateway external dependency secrets, then set up an enterprise subscription + license with a high seat count (for a high quota), and force a Cody Gateway sync: ``` curl -v -H 'Authorization: bearer sekret' http://localhost:9992/-/actor/sync-all-sources ``` This should indicate the new sync against "local dotcom" fetches the correct number of actors and whatnot. Using the local enterprise subscription's access token, we run the QA test suite: ```sh $ bazel test --runs_per_test=2 --test_output=all //cmd/cody-gateway/qa:qa_test --test_env=E2E_GATEWAY_ENDPOINT=http://localhost:9992 --test_env=E2E_GATEWAY_TOKEN=$TOKEN INFO: Analyzed target //cmd/cody-gateway/qa:qa_test (0 packages loaded, 0 targets configured). INFO: From Testing //cmd/cody-gateway/qa:qa_test (run 1 of 2): ==================== Test output for //cmd/cody-gateway/qa:qa_test (run 1 of 2): PASS ================================================================================ INFO: From Testing //cmd/cody-gateway/qa:qa_test (run 2 of 2): ==================== Test output for //cmd/cody-gateway/qa:qa_test (run 2 of 2): PASS ================================================================================ INFO: Found 1 test target... Target //cmd/cody-gateway/qa:qa_test up-to-date: bazel-bin/cmd/cody-gateway/qa/qa_test_/qa_test Aspect @@rules_rust//rust/private:clippy.bzl%rust_clippy_aspect of //cmd/cody-gateway/qa:qa_test up-to-date (nothing to build) Aspect @@rules_rust//rust/private:rustfmt.bzl%rustfmt_aspect of //cmd/cody-gateway/qa:qa_test up-to-date (nothing to build) INFO: Elapsed time: 13.653s, Critical Path: 13.38s INFO: 7 processes: 1 internal, 6 darwin-sandbox. INFO: Build completed successfully, 7 total actions //cmd/cody-gateway/qa:qa_test PASSED in 11.7s Stats over 2 runs: max = 11.7s, min = 11.7s, avg = 11.7s, dev = 0.0s Executed 1 out of 1 test: 1 test passes. ```
198 lines
6.5 KiB
Go
198 lines
6.5 KiB
Go
// Package attribution_test implements a component test for OSS Attribution
|
|
// feature of Cody Enterprise, focusing on the gateway component.
|
|
//
|
|
// ┌───────────┐ ┌────────────┐ ┌─────────┐ ┌────────┐
|
|
// │ │ GraphQL │ │ REST │ │ GraphQL │ │
|
|
// │ Extension ├────────►│ Enterprise ├─────►│ Gateway ├────────►│ Dotcom │
|
|
// │ │ │ instance │ │ │ │ search │
|
|
// └───────────┘ └────────────┘ └─────────┘ └────────┘
|
|
//
|
|
// ! └─── scope of this test ───┘
|
|
// Please see RFC 862 for more detailed design consideration and feature scoping:
|
|
// https://docs.google.com/document/d/1zSxFDQPxZcn5b6yKx40etpJayoibVzj_Gnugzln1weI/view
|
|
package attribution_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Khan/genqlient/graphql"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/sourcegraph/log/logtest"
|
|
|
|
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor"
|
|
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/auth"
|
|
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/dotcom"
|
|
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/events"
|
|
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/httpapi"
|
|
"github.com/sourcegraph/sourcegraph/internal/codygateway"
|
|
)
|
|
|
|
type fakeActorSource struct {
|
|
name codygateway.ActorSource
|
|
}
|
|
|
|
func (s fakeActorSource) Name() string {
|
|
return string(s.name)
|
|
}
|
|
func (s fakeActorSource) Get(context.Context, string) (*actor.Actor, error) {
|
|
return &actor.Actor{Source: s, AccessEnabled: true}, nil
|
|
}
|
|
|
|
// fakeGraphQL is used as test double for dotcom GraphQL API search request.
|
|
// The test runs via HTTP layer exercising also GraphQL code-gen.
|
|
type fakeGraphQL struct {
|
|
t *testing.T // For cleanup and error handling.
|
|
response map[string]any // Nest as deeply as needed.
|
|
url string // For client.
|
|
}
|
|
|
|
func (s *fakeGraphQL) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
if err := json.NewEncoder(w).Encode(s.response); err != nil {
|
|
s.t.Fatalf("fakeGraphQL.ServeHTTP: %s", err)
|
|
}
|
|
}
|
|
|
|
func runFakeGraphQL(t *testing.T) *fakeGraphQL {
|
|
h := &fakeGraphQL{t: t}
|
|
s := httptest.NewServer(h)
|
|
t.Cleanup(s.Close)
|
|
h.url = s.URL
|
|
return h
|
|
}
|
|
|
|
// request creates an attribution search request to the gateway.
|
|
func request(t *testing.T) *http.Request {
|
|
requestBody, err := json.Marshal(&codygateway.AttributionRequest{
|
|
Snippet: strings.Join([]string{
|
|
"for n != 1 {",
|
|
" if n % 2 == 0 {",
|
|
" n = n/2",
|
|
" } else {",
|
|
" n = 3n+1",
|
|
" }",
|
|
"}",
|
|
}, "\n"),
|
|
Limit: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
req, err := http.NewRequest("POST", "/v1/attribution", bytes.NewReader(requestBody))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Authorization", "Bearer sgs_faketoken")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req
|
|
}
|
|
|
|
func TestSuccess(t *testing.T) {
|
|
logger := logtest.Scoped(t)
|
|
ps := fakeActorSource{
|
|
name: codygateway.ActorSourceEnterpriseSubscription,
|
|
}
|
|
authr := &auth.Authenticator{
|
|
Sources: actor.NewSources(ps),
|
|
Logger: logger,
|
|
EventLogger: events.NewStdoutLogger(logger),
|
|
}
|
|
config := &httpapi.Config{EnableAttributionSearch: true}
|
|
fakeDotcom := runFakeGraphQL(t)
|
|
fakeDotcom.response = map[string]any{
|
|
"data": map[string]any{
|
|
"snippetAttribution": map[string]any{
|
|
"nodes": []map[string]any{
|
|
{"repositoryName": "github.com/sourcegraph/sourcegraph"},
|
|
{"repositoryName": "github.com/sourcegraph/cody"},
|
|
},
|
|
"totalCount": 2,
|
|
"limitHit": true,
|
|
},
|
|
},
|
|
}
|
|
dotcomClient := dotcom.NewClient(fakeDotcom.url, "fake auth token", "random", "dev")
|
|
handler, err := httpapi.NewHandler(logger, nil, nil, nil, authr, nil, config, dotcomClient)
|
|
require.NoError(t, err)
|
|
r := request(t)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, r)
|
|
if got, want := w.Code, http.StatusOK; got != want {
|
|
t.Error(w.Body.String())
|
|
t.Fatalf("expected OK, got %d", got)
|
|
}
|
|
var gotResponseBody codygateway.AttributionResponse
|
|
require.NoError(t, json.NewDecoder(w.Body).Decode(&gotResponseBody))
|
|
wantResponseBody := &codygateway.AttributionResponse{
|
|
Repositories: []codygateway.AttributionRepository{
|
|
{Name: "github.com/sourcegraph/sourcegraph"},
|
|
{Name: "github.com/sourcegraph/cody"},
|
|
},
|
|
TotalCount: 2,
|
|
LimitHit: true,
|
|
}
|
|
if diff := cmp.Diff(wantResponseBody, &gotResponseBody); diff != "" {
|
|
t.Fatalf("unespected response (-want+got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// dummyDotComGraphQLApi is the smallest plumbing to wire up nil graphQL client.
|
|
type dummyDotComGraphQLApi struct{}
|
|
|
|
func (g dummyDotComGraphQLApi) MakeRequest(
|
|
ctx context.Context,
|
|
req *graphql.Request,
|
|
resp *graphql.Response,
|
|
) error {
|
|
return nil
|
|
}
|
|
|
|
func TestFailsForDotcomUsers(t *testing.T) {
|
|
logger := logtest.Scoped(t)
|
|
dotCom := fakeActorSource{
|
|
name: codygateway.ActorSourceDotcomUser,
|
|
}
|
|
authr := &auth.Authenticator{
|
|
Sources: actor.NewSources(dotCom),
|
|
Logger: logger,
|
|
EventLogger: events.NewStdoutLogger(logger),
|
|
}
|
|
config := &httpapi.Config{EnableAttributionSearch: true}
|
|
handler, err := httpapi.NewHandler(logger, nil, nil, nil, authr, nil, config, dummyDotComGraphQLApi{})
|
|
require.NoError(t, err)
|
|
r := request(t)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, r)
|
|
if got, want := w.Code, http.StatusUnauthorized; got != want {
|
|
t.Error(w.Body.String())
|
|
t.Fatalf("expected unauthorized, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestUnavailableIfConfigDisabled(t *testing.T) {
|
|
logger := logtest.Scoped(t)
|
|
dotCom := fakeActorSource{
|
|
name: codygateway.ActorSourceEnterpriseSubscription,
|
|
}
|
|
authr := &auth.Authenticator{
|
|
Sources: actor.NewSources(dotCom),
|
|
Logger: logger,
|
|
EventLogger: events.NewStdoutLogger(logger),
|
|
}
|
|
config := &httpapi.Config{}
|
|
handler, err := httpapi.NewHandler(logger, nil, nil, nil, authr, nil, config, dummyDotComGraphQLApi{})
|
|
require.NoError(t, err)
|
|
r := request(t)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, r)
|
|
if got, want := w.Code, http.StatusServiceUnavailable; got != want {
|
|
t.Error(w.Body.String())
|
|
t.Fatalf("expected unauthorized, got %d", got)
|
|
}
|
|
}
|