feat/cody-gateway: use Enterprise Portal for actor/productsubscriptions (#62934)

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:


![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/c61ad4e0-3b75-408d-a930-076a414336fb)

## 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.
```
This commit is contained in:
Robert Lin 2024-06-07 11:46:01 -07:00 committed by GitHub
parent e0a7f7e2b8
commit 7e9d8ec8dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 960 additions and 1247 deletions

View File

@ -75,7 +75,7 @@ func (a *Actor) IsEmpty() bool {
func (a *Actor) IsDotComActor() bool {
// Corresponds to sourcegraph.com subscription ID, or using a dotcom access token
return a != nil && (a.GetSource() == codygateway.ActorSourceProductSubscription && a.ID == "d3d2b638-d0a2-4539-a099-b36860b09819") || a.GetSource() == codygateway.ActorSourceDotcomUser
return a != nil && (a.GetSource() == codygateway.ActorSourceEnterpriseSubscription && a.ID == "d3d2b638-d0a2-4539-a099-b36860b09819") || a.GetSource() == codygateway.ActorSourceDotcomUser
}
type contextKey int

View File

@ -64,7 +64,7 @@ func TestIsDotComActor(t *testing.T) {
t.Run("true for dotcom subscription", func(t *testing.T) {
actor := &Actor{
ID: "d3d2b638-d0a2-4539-a099-b36860b09819",
Source: FakeSource{codygateway.ActorSourceProductSubscription},
Source: FakeSource{codygateway.ActorSourceEnterpriseSubscription},
}
require.True(t, actor.IsDotComActor())
})
@ -79,7 +79,7 @@ func TestIsDotComActor(t *testing.T) {
t.Run("false for other subscription", func(t *testing.T) {
actor := &Actor{
ID: "other-sub-id",
Source: FakeSource{codygateway.ActorSourceProductSubscription},
Source: FakeSource{codygateway.ActorSourceEnterpriseSubscription},
}
require.False(t, actor.IsDotComActor())
})

View File

@ -9,20 +9,21 @@ go_library(
visibility = ["//cmd/cody-gateway:__subpackages__"],
deps = [
"//cmd/cody-gateway/internal/actor",
"//cmd/cody-gateway/internal/dotcom",
"//internal/codygateway",
"//internal/collections",
"//internal/license",
"//internal/licensing",
"//internal/productsubscription",
"//internal/trace",
"//lib/enterpriseportal/codyaccess/v1:codyaccess",
"//lib/enterpriseportal/subscriptions/v1:subscriptions",
"//lib/errors",
"@com_github_gregjones_httpcache//:httpcache",
"@com_github_khan_genqlient//graphql",
"@com_github_sourcegraph_log//:log",
"@com_github_vektah_gqlparser_v2//gqlerror",
"@io_opentelemetry_go_otel//attribute",
"@io_opentelemetry_go_otel_trace//:trace",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status",
],
)
@ -32,11 +33,14 @@ go_test(
embed = [":productsubscription"],
tags = [TAG_CODY_PRIME],
deps = [
"//cmd/cody-gateway/internal/dotcom",
"//internal/codygateway",
"//internal/collections",
"//lib/enterpriseportal/codyaccess/v1:codyaccess",
"@com_github_hexops_autogold_v2//:autogold",
"@com_github_sourcegraph_log//:log",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_golang_google_protobuf//types/known/durationpb",
"@org_golang_x_exp//maps",
],
)

View File

@ -0,0 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "enterpriseportal",
srcs = ["enterpriseportal.go"],
importpath = "github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription/enterpriseportal",
visibility = ["//cmd/cody-gateway:__subpackages__"],
deps = [
"//internal/env",
"//internal/grpc/defaults",
"//internal/grpc/grpcoauth",
"//lib/errors",
"@com_github_sourcegraph_log//:log",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_x_oauth2//:oauth2",
],
)

View File

@ -0,0 +1,39 @@
package enterpriseportal
import (
"context"
"net/url"
"github.com/sourcegraph/log"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/internal/grpc/defaults"
"github.com/sourcegraph/sourcegraph/internal/grpc/grpcoauth"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// Dial establishes a connection to the Enterprise Portal gRPC service with
// the given configuration. The oauth2.TokenSource should provide SAMS credentials.
func Dial(ctx context.Context, logger log.Logger, addr *url.URL, ts oauth2.TokenSource) (*grpc.ClientConn, error) {
insecureTarget := addr.Scheme != "https"
if insecureTarget && !env.InsecureDev {
return nil, errors.New("insecure target Enterprise Portal used outside of dev mode")
}
creds := grpc.WithPerRPCCredentials(grpcoauth.TokenSource{TokenSource: ts})
var opts []grpc.DialOption
if insecureTarget {
opts = defaults.DialOptions(logger, creds)
} else {
opts = defaults.ExternalDialOptions(logger, creds)
}
logger.Info("dialing Enterprise Portal gRPC service",
log.String("host", addr.Host),
log.Bool("insecureTarget", insecureTarget))
conn, err := grpc.DialContext(ctx, addr.Host, opts...)
if err != nil {
return nil, errors.Wrapf(err, "failed to connect to Enterprise Portal gRPC service at %s", addr.String())
}
return conn, nil
}

View File

@ -3,27 +3,27 @@ package productsubscription
import (
"context"
"encoding/json"
"slices"
"strings"
"time"
"github.com/Khan/genqlient/graphql"
"github.com/gregjones/httpcache"
"github.com/sourcegraph/log"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/sourcegraph/sourcegraph/internal/codygateway"
"github.com/sourcegraph/sourcegraph/internal/collections"
"github.com/sourcegraph/sourcegraph/internal/license"
"github.com/sourcegraph/sourcegraph/internal/licensing"
"github.com/sourcegraph/sourcegraph/internal/productsubscription"
sgtrace "github.com/sourcegraph/sourcegraph/internal/trace"
codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1"
subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/dotcom"
)
// SourceVersion should be bumped whenever the format of any cached data in this
@ -45,14 +45,20 @@ type ListingCache interface {
ListAllKeys() []string
}
type Source struct {
log log.Logger
cache ListingCache // cache is expected to be something with automatic TTL
dotcom graphql.Client
// EnterprisePortalClient defines the RPCs implemented by the Enterprise Portal
// that this Source depends on. We declare our own interface to keep our
// generated mock surface minimal, and our dependencies explicit.
type EnterprisePortalClient interface {
// codyaccessv1 RPCs
GetCodyGatewayAccess(context.Context, *codyaccessv1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*codyaccessv1.GetCodyGatewayAccessResponse, error)
ListCodyGatewayAccesses(context.Context, *codyaccessv1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*codyaccessv1.ListCodyGatewayAccessesResponse, error)
}
// internalMode, if true, indicates only dev and internal licenses may use
// this Cody Gateway instance.
internalMode bool
type Source struct {
log log.Logger
cache ListingCache // cache is expected to be something with automatic TTL
enterprisePortal EnterprisePortalClient
concurrencyConfig codygateway.ActorConcurrencyLimitConfig
}
@ -61,19 +67,23 @@ var _ actor.Source = &Source{}
var _ actor.SourceUpdater = &Source{}
var _ actor.SourceSyncer = &Source{}
func NewSource(logger log.Logger, cache ListingCache, dotcomClient graphql.Client, internalMode bool, concurrencyConfig codygateway.ActorConcurrencyLimitConfig) *Source {
func NewSource(
logger log.Logger,
cache ListingCache,
enterprisePortal EnterprisePortalClient,
concurrencyConfig codygateway.ActorConcurrencyLimitConfig,
) *Source {
return &Source{
log: logger.Scoped("productsubscriptions"),
cache: cache,
dotcom: dotcomClient,
log: logger.Scoped("productsubscriptions"),
cache: cache,
internalMode: internalMode,
enterprisePortal: enterprisePortal,
concurrencyConfig: concurrencyConfig,
}
}
func (s *Source) Name() string { return string(codygateway.ActorSourceProductSubscription) }
func (s *Source) Name() string { return string(codygateway.ActorSourceEnterpriseSubscription) }
func (s *Source) Get(ctx context.Context, token string) (*actor.Actor, error) {
if token == "" {
@ -139,23 +149,30 @@ func (s *Source) Sync(ctx context.Context) (seen int, errs error) {
syncLog := sgtrace.Logger(ctx, s.log)
seenTokens := collections.NewSet[string]()
resp, err := dotcom.ListProductSubscriptions(ctx, s.dotcom)
resp, err := s.enterprisePortal.ListCodyGatewayAccesses(ctx, &codyaccessv1.ListCodyGatewayAccessesRequest{
// TODO(https://linear.app/sourcegraph/issue/CORE-134): Once the
// Enterprise Portal supports pagination in its API responses we need to
// update this callsite to make repeated requests with a continuation
// token, etc. For now, we assume that we are fetching all licenses in
// a single call.
})
if err != nil {
if errors.Is(err, context.Canceled) {
syncLog.Warn("sync context cancelled")
return seen, nil
}
return seen, errors.Wrap(err, "failed to list subscriptions from dotcom")
return seen, errors.Wrap(err, "failed to list Enterprise subscriptions")
}
for _, sub := range resp.Dotcom.ProductSubscriptions.Nodes {
for _, token := range sub.SourcegraphAccessTokens {
for _, access := range resp.GetAccesses() {
for _, token := range access.GetAccessTokens() {
select {
case <-ctx.Done():
return seen, ctx.Err()
default:
}
act := newActor(s, token, sub.ProductSubscriptionState, s.internalMode, s.concurrencyConfig)
act := newActor(s, token.GetToken(), access, time.Now())
data, err := json.Marshal(act)
if err != nil {
act.Logger(syncLog).Error("failed to marshal actor",
@ -163,8 +180,8 @@ func (s *Source) Sync(ctx context.Context) (seen int, errs error) {
errs = errors.Append(errs, err)
continue
}
s.cache.Set(token, data)
seenTokens.Add(token)
s.cache.Set(token.GetToken(), data)
seenTokens.Add(token.GetToken())
seen++
}
}
@ -198,27 +215,25 @@ func removeUnseenTokens(seen collections.Set[string], cache ListingCache, syncLo
}
}
func (s *Source) checkAccessToken(ctx context.Context, token string) (*dotcom.CheckAccessTokenResponse, error) {
resp, err := dotcom.CheckAccessToken(ctx, s.dotcom, token)
func (s *Source) checkAccessToken(ctx context.Context, token string) (*codyaccessv1.CodyGatewayAccess, error) {
resp, err := s.enterprisePortal.GetCodyGatewayAccess(ctx, &codyaccessv1.GetCodyGatewayAccessRequest{
Query: &codyaccessv1.GetCodyGatewayAccessRequest_AccessToken{
AccessToken: token,
},
})
if err == nil {
return resp, nil
return resp.GetAccess(), nil
}
// Inspect the error to see if it's a list of GraphQL errors.
gqlerrs, ok := err.(gqlerror.List)
if !ok {
return nil, err
}
for _, gqlerr := range gqlerrs {
if gqlerr.Extensions != nil && gqlerr.Extensions["code"] == productsubscription.GQLErrCodeProductSubscriptionNotFound {
return nil, actor.ErrAccessTokenDenied{
Source: s.Name(),
Reason: "associated product subscription not found",
}
// Inspect the error to see if it's not-found error.
if statusErr, ok := status.FromError(err); ok && statusErr.Code() == codes.NotFound {
return nil, actor.ErrAccessTokenDenied{
Source: s.Name(),
Reason: "associated product subscription not found",
}
}
return nil, err
return nil, errors.Wrap(err, "verifying token via Enterprise Portal")
}
func (s *Source) fetchAndCache(ctx context.Context, token string) (*actor.Actor, error) {
@ -226,14 +241,13 @@ func (s *Source) fetchAndCache(ctx context.Context, token string) (*actor.Actor,
resp, checkErr := s.checkAccessToken(ctx, token)
if checkErr != nil {
// Generate a stateless actor so that we aren't constantly hitting the dotcom API
act = newActor(s, token, dotcom.ProductSubscriptionState{}, s.internalMode, s.concurrencyConfig)
act = newActor(s, token, &codyaccessv1.CodyGatewayAccess{}, time.Now())
} else {
act = newActor(
s,
token,
resp.Dotcom.ProductSubscriptionByAccessToken.ProductSubscriptionState,
s.internalMode,
s.concurrencyConfig,
resp,
time.Now(),
)
}
@ -252,86 +266,73 @@ func (s *Source) fetchAndCache(ctx context.Context, token string) (*actor.Actor,
// getSubscriptionAccountName attempts to get the account name from the product
// subscription. It returns an empty string if no account name is available.
func getSubscriptionAccountName(s dotcom.ProductSubscriptionState) string {
// 1. Check if the special "customer:" tag is present
if s.ActiveLicense != nil && s.ActiveLicense.Info != nil {
for _, tag := range s.ActiveLicense.Info.Tags {
if strings.HasPrefix(tag, "customer:") {
return strings.TrimPrefix(tag, "customer:")
}
func getSubscriptionAccountName(activeLicenseTags []string) string {
// Check if the special "customer:" tag is present.
for _, tag := range activeLicenseTags {
if strings.HasPrefix(tag, "customer:") {
return strings.TrimPrefix(tag, "customer:")
}
}
// 2. Use the username of the account
if s.Account != nil && s.Account.Username != "" {
return s.Account.Username
}
return ""
}
// newActor creates an actor from Sourcegraph.com product subscription state.
func newActor(source *Source, token string, s dotcom.ProductSubscriptionState, internalMode bool, concurrencyConfig codygateway.ActorConcurrencyLimitConfig) *actor.Actor {
name := getSubscriptionAccountName(s)
func newActor(
source *Source,
token string,
s *codyaccessv1.CodyGatewayAccess,
now time.Time,
) *actor.Actor {
name := s.GetSubscriptionDisplayName()
if name == "" {
name = s.Uuid
name = s.GetSubscriptionId()
}
// In internal mode, only allow dev and internal licenses.
disallowedLicense := internalMode &&
(s.ActiveLicense == nil || s.ActiveLicense.Info == nil ||
!containsOneOf(s.ActiveLicense.Info.Tags, licensing.DevTag, licensing.InternalTag))
now := time.Now()
a := &actor.Actor{
Key: token,
ID: s.Uuid,
Key: token,
// Maintain consistency with existing non-prefixed IDs.
ID: strings.TrimPrefix(s.GetSubscriptionId(), subscriptionsv1.EnterpriseSubscriptionIDPrefix),
Name: name,
AccessEnabled: !disallowedLicense && !s.IsArchived && s.CodyGatewayAccess.Enabled,
AccessEnabled: s.GetEnabled(),
EndpointAccess: map[string]bool{
"/v1/attribution": !disallowedLicense && !s.IsArchived,
// Always enabled even if !s.GetEnabled(), to allow BYOK customers.
"/v1/attribution": true,
},
RateLimits: map[codygateway.Feature]actor.RateLimit{},
LastUpdated: &now,
Source: source,
}
if rl := s.CodyGatewayAccess.ChatCompletionsRateLimit; rl != nil {
if rl := s.GetChatCompletionsRateLimit(); rl != nil {
a.RateLimits[codygateway.FeatureChatCompletions] = actor.NewRateLimitWithPercentageConcurrency(
int64(rl.Limit),
time.Duration(rl.IntervalSeconds)*time.Second,
rl.IntervalDuration.AsDuration(),
[]string{"*"}, // allow all models that are allowlisted by Cody Gateway
concurrencyConfig,
source.concurrencyConfig,
)
}
if rl := s.CodyGatewayAccess.CodeCompletionsRateLimit; rl != nil {
if rl := s.GetCodeCompletionsRateLimit(); rl != nil {
a.RateLimits[codygateway.FeatureCodeCompletions] = actor.NewRateLimitWithPercentageConcurrency(
int64(rl.Limit),
time.Duration(rl.IntervalSeconds)*time.Second,
rl.IntervalDuration.AsDuration(),
[]string{"*"}, // allow all models that are allowlisted by Cody Gateway
concurrencyConfig,
source.concurrencyConfig,
)
}
if rl := s.CodyGatewayAccess.EmbeddingsRateLimit; rl != nil {
if rl := s.GetEmbeddingsRateLimit(); rl != nil {
a.RateLimits[codygateway.FeatureEmbeddings] = actor.NewRateLimitWithPercentageConcurrency(
int64(rl.Limit),
time.Duration(rl.IntervalSeconds)*time.Second,
rl.IntervalDuration.AsDuration(),
[]string{"*"}, // allow all models that are allowlisted by Cody Gateway
// TODO: Once we split interactive and on-interactive, we want to apply
// stricter limits here than percentage based for this heavy endpoint.
concurrencyConfig,
source.concurrencyConfig,
)
}
return a
}
func containsOneOf(s []string, needles ...string) bool {
for _, needle := range needles {
if slices.Contains(s, needle) {
return true
}
}
return false
}

View File

@ -1,160 +1,173 @@
package productsubscription
import (
"encoding/json"
"testing"
"time"
"github.com/hexops/autogold/v2"
"github.com/sourcegraph/log"
"github.com/sourcegraph/sourcegraph/internal/collections"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/dotcom"
"github.com/sourcegraph/sourcegraph/internal/codygateway"
"github.com/sourcegraph/sourcegraph/internal/collections"
codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1"
)
func TestNewActor(t *testing.T) {
concurrencyConfig := codygateway.ActorConcurrencyLimitConfig{
Percentage: 50,
Interval: 24 * time.Hour,
}
type args struct {
s dotcom.ProductSubscriptionState
devLicensesOnly bool
access *codyaccessv1.CodyGatewayAccess
}
tests := []struct {
name string
args args
wantEnabled bool
name string
args args
wantActor autogold.Value
}{
{
name: "not dev only",
name: "enabled, no embeddings",
args: args{
dotcom.ProductSubscriptionState{
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
&codyaccessv1.CodyGatewayAccess{
SubscriptionId: "es_1234uuid",
SubscriptionDisplayName: "My Subscription",
Enabled: true,
ChatCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{
Limit: 10,
IntervalDuration: durationpb.New(10 * time.Second),
},
CodeCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{
Limit: 10,
IntervalDuration: durationpb.New(10 * time.Second),
},
},
false,
},
wantEnabled: true,
wantActor: autogold.Expect(`{
"key": "sekret_token",
"id": "1234uuid",
"name": "My Subscription",
"accessEnabled": true,
"endpointAccess": {
"/v1/attribution": true
},
"rateLimits": {
"chat_completions": {
"allowedModels": [
"*"
],
"limit": 10,
"interval": 10000000000,
"concurrentRequests": 4320000,
"concurrentRequestsInterval": 86400000000000
},
"code_completions": {
"allowedModels": [
"*"
],
"limit": 10,
"interval": 10000000000,
"concurrentRequests": 4320000,
"concurrentRequestsInterval": 86400000000000
}
},
"lastUpdated": "2024-06-03T20:03:07-07:00"
}`),
},
{
name: "dev only, not a dev license",
name: "enabled, only embeddings",
args: args{
dotcom.ProductSubscriptionState{
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
&codyaccessv1.CodyGatewayAccess{
SubscriptionId: "es_1234uuid",
SubscriptionDisplayName: "My Subscription",
Enabled: true,
EmbeddingsRateLimit: &codyaccessv1.CodyGatewayRateLimit{
Limit: 10,
IntervalDuration: durationpb.New(10 * time.Second),
},
},
true,
},
wantEnabled: false,
wantActor: autogold.Expect(`{
"key": "sekret_token",
"id": "1234uuid",
"name": "My Subscription",
"accessEnabled": true,
"endpointAccess": {
"/v1/attribution": true
},
"rateLimits": {
"embeddings": {
"allowedModels": [
"*"
],
"limit": 10,
"interval": 10000000000,
"concurrentRequests": 4320000,
"concurrentRequestsInterval": 86400000000000
}
},
"lastUpdated": "2024-06-03T20:03:07-07:00"
}`),
},
{
name: "dev only, is a dev license",
name: "disabled",
args: args{
dotcom.ProductSubscriptionState{
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
},
ActiveLicense: &dotcom.ProductSubscriptionStateActiveLicenseProductLicense{
Info: &dotcom.ProductSubscriptionStateActiveLicenseProductLicenseInfo{
Tags: []string{"dev"},
},
},
&codyaccessv1.CodyGatewayAccess{
SubscriptionId: "es_1234uuid",
SubscriptionDisplayName: "My Subscription",
Enabled: false,
},
true,
},
wantEnabled: true,
wantActor: autogold.Expect(`{
"key": "sekret_token",
"id": "1234uuid",
"name": "My Subscription",
"accessEnabled": false,
"endpointAccess": {
"/v1/attribution": true
},
"rateLimits": {},
"lastUpdated": "2024-06-03T20:03:07-07:00"
}`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
act := newActor(nil, "", tt.args.s, tt.args.devLicensesOnly, concurrencyConfig)
assert.Equal(t, act.AccessEnabled, tt.wantEnabled)
now := time.Date(2024, 6, 3, 20, 3, 7, 0, time.FixedZone("PDT", -25200))
act := newActor(&Source{
concurrencyConfig: codygateway.ActorConcurrencyLimitConfig{
Percentage: 50,
Interval: 24 * time.Hour,
},
}, "sekret_token", tt.args.access, now)
// Assert against JSON representation, because that's what we end
// up caching.
actData, err := json.MarshalIndent(act, "", " ")
require.NoError(t, err)
tt.wantActor.Equal(t, string(actData))
})
}
}
func TestGetSubscriptionAccountName(t *testing.T) {
tests := []struct {
name string
mockUsername string
mockTags []string
wantName string
name string
mockTags []string
wantName string
}{
{
name: "has special license tag",
mockUsername: "alice",
mockTags: []string{"trial", "customer:acme"},
wantName: "acme",
name: "has special license tag",
mockTags: []string{"trial", "customer:acme"},
wantName: "acme",
},
{
name: "use account username",
mockUsername: "alice",
mockTags: []string{"plan:enterprise-1"},
wantName: "alice",
},
{
name: "no account name",
name: "no data",
wantName: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := getSubscriptionAccountName(dotcom.ProductSubscriptionState{
Account: &dotcom.ProductSubscriptionStateAccountUser{
Username: test.mockUsername,
},
ActiveLicense: &dotcom.ProductSubscriptionStateActiveLicenseProductLicense{
Info: &dotcom.ProductSubscriptionStateActiveLicenseProductLicenseInfo{
Tags: test.mockTags,
},
},
})
got := getSubscriptionAccountName(test.mockTags)
assert.Equal(t, test.wantName, got)
})
}

View File

@ -0,0 +1,25 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//dev:go_mockgen.bzl", "go_mockgen")
go_library(
name = "productsubscriptiontest",
srcs = ["mocks.go"],
importpath = "github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription/productsubscriptiontest",
visibility = ["//cmd/cody-gateway:__subpackages__"],
deps = [
"//cmd/cody-gateway/internal/actor/productsubscription",
"//lib/enterpriseportal/codyaccess/v1:codyaccess",
"@org_golang_google_grpc//:go_default_library",
],
)
go_mockgen(
name = "generate_mocks",
out = "mocks.go",
manifests = [
"//:mockgen.yaml",
"//:mockgen.test.yaml",
"//:mockgen.temp.yaml",
],
deps = ["//cmd/cody-gateway/internal/actor/productsubscription"],
)

View File

@ -0,0 +1,323 @@
// Code generated by go-mockgen 1.3.7; DO NOT EDIT.
//
// This file was generated by running `sg generate` (or `go-mockgen`) at the root of
// this repository. To add additional mocks to this or another package, add a new entry
// to the mockgen.yaml file in the root of this repository.
package productsubscriptiontest
import (
"context"
"sync"
productsubscription "github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription"
v1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1"
grpc "google.golang.org/grpc"
)
// MockEnterprisePortalClient is a mock implementation of the
// EnterprisePortalClient interface (from the package
// github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription)
// used for unit testing.
type MockEnterprisePortalClient struct {
// GetCodyGatewayAccessFunc is an instance of a mock function object
// controlling the behavior of the method GetCodyGatewayAccess.
GetCodyGatewayAccessFunc *EnterprisePortalClientGetCodyGatewayAccessFunc
// ListCodyGatewayAccessesFunc is an instance of a mock function object
// controlling the behavior of the method ListCodyGatewayAccesses.
ListCodyGatewayAccessesFunc *EnterprisePortalClientListCodyGatewayAccessesFunc
}
// NewMockEnterprisePortalClient creates a new mock of the
// EnterprisePortalClient interface. All methods return zero values for all
// results, unless overwritten.
func NewMockEnterprisePortalClient() *MockEnterprisePortalClient {
return &MockEnterprisePortalClient{
GetCodyGatewayAccessFunc: &EnterprisePortalClientGetCodyGatewayAccessFunc{
defaultHook: func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (r0 *v1.GetCodyGatewayAccessResponse, r1 error) {
return
},
},
ListCodyGatewayAccessesFunc: &EnterprisePortalClientListCodyGatewayAccessesFunc{
defaultHook: func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (r0 *v1.ListCodyGatewayAccessesResponse, r1 error) {
return
},
},
}
}
// NewStrictMockEnterprisePortalClient creates a new mock of the
// EnterprisePortalClient interface. All methods panic on invocation, unless
// overwritten.
func NewStrictMockEnterprisePortalClient() *MockEnterprisePortalClient {
return &MockEnterprisePortalClient{
GetCodyGatewayAccessFunc: &EnterprisePortalClientGetCodyGatewayAccessFunc{
defaultHook: func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error) {
panic("unexpected invocation of MockEnterprisePortalClient.GetCodyGatewayAccess")
},
},
ListCodyGatewayAccessesFunc: &EnterprisePortalClientListCodyGatewayAccessesFunc{
defaultHook: func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error) {
panic("unexpected invocation of MockEnterprisePortalClient.ListCodyGatewayAccesses")
},
},
}
}
// NewMockEnterprisePortalClientFrom creates a new mock of the
// MockEnterprisePortalClient interface. All methods delegate to the given
// implementation, unless overwritten.
func NewMockEnterprisePortalClientFrom(i productsubscription.EnterprisePortalClient) *MockEnterprisePortalClient {
return &MockEnterprisePortalClient{
GetCodyGatewayAccessFunc: &EnterprisePortalClientGetCodyGatewayAccessFunc{
defaultHook: i.GetCodyGatewayAccess,
},
ListCodyGatewayAccessesFunc: &EnterprisePortalClientListCodyGatewayAccessesFunc{
defaultHook: i.ListCodyGatewayAccesses,
},
}
}
// EnterprisePortalClientGetCodyGatewayAccessFunc describes the behavior
// when the GetCodyGatewayAccess method of the parent
// MockEnterprisePortalClient instance is invoked.
type EnterprisePortalClientGetCodyGatewayAccessFunc struct {
defaultHook func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error)
hooks []func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error)
history []EnterprisePortalClientGetCodyGatewayAccessFuncCall
mutex sync.Mutex
}
// GetCodyGatewayAccess delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockEnterprisePortalClient) GetCodyGatewayAccess(v0 context.Context, v1 *v1.GetCodyGatewayAccessRequest, v2 ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error) {
r0, r1 := m.GetCodyGatewayAccessFunc.nextHook()(v0, v1, v2...)
m.GetCodyGatewayAccessFunc.appendCall(EnterprisePortalClientGetCodyGatewayAccessFuncCall{v0, v1, v2, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetCodyGatewayAccess
// method of the parent MockEnterprisePortalClient instance is invoked and
// the hook queue is empty.
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) SetDefaultHook(hook func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetCodyGatewayAccess method of the parent MockEnterprisePortalClient
// instance invokes the hook at the front of the queue and discards it.
// After the queue is empty, the default hook function is invoked for any
// future action.
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) PushHook(hook func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) SetDefaultReturn(r0 *v1.GetCodyGatewayAccessResponse, r1 error) {
f.SetDefaultHook(func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) PushReturn(r0 *v1.GetCodyGatewayAccessResponse, r1 error) {
f.PushHook(func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error) {
return r0, r1
})
}
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) nextHook() func(context.Context, *v1.GetCodyGatewayAccessRequest, ...grpc.CallOption) (*v1.GetCodyGatewayAccessResponse, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) appendCall(r0 EnterprisePortalClientGetCodyGatewayAccessFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// EnterprisePortalClientGetCodyGatewayAccessFuncCall objects describing the
// invocations of this function.
func (f *EnterprisePortalClientGetCodyGatewayAccessFunc) History() []EnterprisePortalClientGetCodyGatewayAccessFuncCall {
f.mutex.Lock()
history := make([]EnterprisePortalClientGetCodyGatewayAccessFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// EnterprisePortalClientGetCodyGatewayAccessFuncCall is an object that
// describes an invocation of method GetCodyGatewayAccess on an instance of
// MockEnterprisePortalClient.
type EnterprisePortalClientGetCodyGatewayAccessFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 *v1.GetCodyGatewayAccessRequest
// Arg2 is a slice containing the values of the variadic arguments
// passed to this method invocation.
Arg2 []grpc.CallOption
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *v1.GetCodyGatewayAccessResponse
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation. The variadic slice argument is flattened in this array such
// that one positional argument and three variadic arguments would result in
// a slice of four, not two.
func (c EnterprisePortalClientGetCodyGatewayAccessFuncCall) Args() []interface{} {
trailing := []interface{}{}
for _, val := range c.Arg2 {
trailing = append(trailing, val)
}
return append([]interface{}{c.Arg0, c.Arg1}, trailing...)
}
// Results returns an interface slice containing the results of this
// invocation.
func (c EnterprisePortalClientGetCodyGatewayAccessFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// EnterprisePortalClientListCodyGatewayAccessesFunc describes the behavior
// when the ListCodyGatewayAccesses method of the parent
// MockEnterprisePortalClient instance is invoked.
type EnterprisePortalClientListCodyGatewayAccessesFunc struct {
defaultHook func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error)
hooks []func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error)
history []EnterprisePortalClientListCodyGatewayAccessesFuncCall
mutex sync.Mutex
}
// ListCodyGatewayAccesses delegates to the next hook function in the queue
// and stores the parameter and result values of this invocation.
func (m *MockEnterprisePortalClient) ListCodyGatewayAccesses(v0 context.Context, v1 *v1.ListCodyGatewayAccessesRequest, v2 ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error) {
r0, r1 := m.ListCodyGatewayAccessesFunc.nextHook()(v0, v1, v2...)
m.ListCodyGatewayAccessesFunc.appendCall(EnterprisePortalClientListCodyGatewayAccessesFuncCall{v0, v1, v2, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the
// ListCodyGatewayAccesses method of the parent MockEnterprisePortalClient
// instance is invoked and the hook queue is empty.
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) SetDefaultHook(hook func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// ListCodyGatewayAccesses method of the parent MockEnterprisePortalClient
// instance invokes the hook at the front of the queue and discards it.
// After the queue is empty, the default hook function is invoked for any
// future action.
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) PushHook(hook func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) SetDefaultReturn(r0 *v1.ListCodyGatewayAccessesResponse, r1 error) {
f.SetDefaultHook(func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) PushReturn(r0 *v1.ListCodyGatewayAccessesResponse, r1 error) {
f.PushHook(func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error) {
return r0, r1
})
}
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) nextHook() func(context.Context, *v1.ListCodyGatewayAccessesRequest, ...grpc.CallOption) (*v1.ListCodyGatewayAccessesResponse, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) appendCall(r0 EnterprisePortalClientListCodyGatewayAccessesFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of
// EnterprisePortalClientListCodyGatewayAccessesFuncCall objects describing
// the invocations of this function.
func (f *EnterprisePortalClientListCodyGatewayAccessesFunc) History() []EnterprisePortalClientListCodyGatewayAccessesFuncCall {
f.mutex.Lock()
history := make([]EnterprisePortalClientListCodyGatewayAccessesFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// EnterprisePortalClientListCodyGatewayAccessesFuncCall is an object that
// describes an invocation of method ListCodyGatewayAccesses on an instance
// of MockEnterprisePortalClient.
type EnterprisePortalClientListCodyGatewayAccessesFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 *v1.ListCodyGatewayAccessesRequest
// Arg2 is a slice containing the values of the variadic arguments
// passed to this method invocation.
Arg2 []grpc.CallOption
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *v1.ListCodyGatewayAccessesResponse
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation. The variadic slice argument is flattened in this array such
// that one positional argument and three variadic arguments would result in
// a slice of four, not two.
func (c EnterprisePortalClientListCodyGatewayAccessesFuncCall) Args() []interface{} {
trailing := []interface{}{}
for _, val := range c.Arg2 {
trailing = append(trailing, val)
}
return append([]interface{}{c.Arg0, c.Arg1}, trailing...)
}
// Results returns an interface slice containing the results of this
// invocation.
func (c EnterprisePortalClientListCodyGatewayAccessesFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}

View File

@ -149,7 +149,11 @@ func (s *Sources) SyncAll(ctx context.Context, logger log.Logger) error {
if err != nil {
return errors.Wrapf(err, "failed to sync %s", src.Name())
}
syncLogger.Info("Completed sync", log.Duration("sync_duration", time.Since(start)), log.Int("seen", seen))
span.SetAttributes(
attribute.Int("seen_actors", seen))
syncLogger.Info("Completed sync",
log.Duration("sync_duration", time.Since(start)),
log.Int("seen", seen))
return nil
})
}

View File

@ -32,18 +32,18 @@ go_test(
"//cmd/cody-gateway/internal/actor",
"//cmd/cody-gateway/internal/actor/anonymous",
"//cmd/cody-gateway/internal/actor/productsubscription",
"//cmd/cody-gateway/internal/dotcom",
"//cmd/cody-gateway/internal/actor/productsubscription/productsubscriptiontest",
"//cmd/cody-gateway/internal/events",
"//internal/codygateway",
"//internal/licensing",
"//internal/productsubscription",
"//lib/enterpriseportal/codyaccess/v1:codyaccess",
"//lib/errors",
"@com_github_derision_test_go_mockgen_v2//testutil/require",
"@com_github_khan_genqlient//graphql",
"@com_github_sourcegraph_log//logtest",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@com_github_vektah_gqlparser_v2//gqlerror",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status",
"@org_golang_google_protobuf//types/known/durationpb",
],
)

View File

@ -2,28 +2,27 @@ package auth
// pre-commit:ignore_sourcegraph_token
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/Khan/genqlient/graphql"
mockrequire "github.com/derision-test/go-mockgen/v2/testutil/require"
"github.com/sourcegraph/log/logtest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vektah/gqlparser/v2/gqlerror"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/anonymous"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/dotcom"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription/productsubscriptiontest"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/events"
"github.com/sourcegraph/sourcegraph/internal/codygateway"
"github.com/sourcegraph/sourcegraph/internal/licensing"
internalproductsubscription "github.com/sourcegraph/sourcegraph/internal/productsubscription"
codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
@ -58,36 +57,25 @@ func TestAuthenticatorMiddleware(t *testing.T) {
t.Run("authenticated without cache hit", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
resp.Data.(*dotcom.CheckAccessTokenResponse).Dotcom = dotcom.CheckAccessTokenDotcomDotcomQuery{
ProductSubscriptionByAccessToken: dotcom.CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription{
ProductSubscriptionState: dotcom.ProductSubscriptionState{
Id: "UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==",
Uuid: "6452a8fc-e650-45a7-a0a2-357f776b3b46",
IsArchived: false,
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
},
client := productsubscriptiontest.NewMockEnterprisePortalClient()
client.GetCodyGatewayAccessFunc.PushReturn(
&codyaccessv1.GetCodyGatewayAccessResponse{
Access: &codyaccessv1.CodyGatewayAccess{
SubscriptionId: "es_6452a8fc-e650-45a7-a0a2-357f776b3b46",
Enabled: true,
ChatCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{
Limit: 10,
IntervalDuration: durationpb.New(10 * time.Second),
},
CodeCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{
Limit: 10,
IntervalDuration: durationpb.New(10 * time.Second),
},
},
}
return nil
})
},
nil,
)
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.NotNil(t, actor.FromContext(r.Context()))
w.WriteHeader(http.StatusOK)
@ -99,10 +87,10 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, false, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
mockrequire.Called(t, client.MakeRequestFunc)
mockrequire.Called(t, client.GetCodyGatewayAccessFunc)
})
t.Run("authenticated with cache hit", func(t *testing.T) {
@ -111,7 +99,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
[]byte(`{"id":"UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==","accessEnabled":true,"rateLimit":null}`),
true,
)
client := dotcom.NewMockClient()
client := productsubscriptiontest.NewMockEnterprisePortalClient()
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.NotNil(t, actor.FromContext(r.Context()))
w.WriteHeader(http.StatusOK)
@ -123,10 +111,10 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, false, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
mockrequire.NotCalled(t, client.MakeRequestFunc)
mockrequire.NotCalled(t, client.GetCodyGatewayAccessFunc)
})
t.Run("authenticated but not enabled", func(t *testing.T) {
@ -135,7 +123,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
[]byte(`{"id":"UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==","accessEnabled":false,"rateLimit":null}`),
true,
)
client := dotcom.NewMockClient()
client := productsubscriptiontest.NewMockEnterprisePortalClient()
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
@ -143,7 +131,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, false, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusForbidden, w.Code)
})
@ -154,7 +142,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
[]byte(`{"id":"UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==","accessEnabled":false,"endpointAccess":{"/v1/attribution":true},"rateLimit":null}`),
true,
)
client := dotcom.NewMockClient()
client := productsubscriptiontest.NewMockEnterprisePortalClient()
t.Run("bypass works for attribution", func(t *testing.T) {
w := httptest.NewRecorder()
@ -163,7 +151,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, false, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
@ -175,7 +163,7 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, false, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusForbidden, w.Code)
})
@ -183,15 +171,11 @@ func TestAuthenticatorMiddleware(t *testing.T) {
t.Run("access token denied from sources", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
return gqlerror.List{
{
Message: "access denied",
Extensions: map[string]any{"code": internalproductsubscription.GQLErrCodeProductSubscriptionNotFound},
},
}
})
client := productsubscriptiontest.NewMockEnterprisePortalClient()
client.GetCodyGatewayAccessFunc.PushReturn(
nil,
status.Error(codes.NotFound, "not found"),
)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
@ -199,17 +183,19 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, true, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusUnauthorized, w.Code)
mockrequire.Called(t, client.GetCodyGatewayAccessFunc)
})
t.Run("server error from sources", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
return errors.New("server error")
})
client := productsubscriptiontest.NewMockEnterprisePortalClient()
client.GetCodyGatewayAccessFunc.SetDefaultReturn(
nil,
errors.New("server error"),
)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
@ -217,155 +203,8 @@ func TestAuthenticatorMiddleware(t *testing.T) {
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, true, concurrencyConfig)),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
})
t.Run("internal mode, authenticated but not dev license", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
resp.Data.(*dotcom.CheckAccessTokenResponse).Dotcom = dotcom.CheckAccessTokenDotcomDotcomQuery{
ProductSubscriptionByAccessToken: dotcom.CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription{
ProductSubscriptionState: dotcom.ProductSubscriptionState{
Id: "UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==",
Uuid: "6452a8fc-e650-45a7-a0a2-357f776b3b46",
IsArchived: false,
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
},
ActiveLicense: &dotcom.ProductSubscriptionStateActiveLicenseProductLicense{
Info: &dotcom.ProductSubscriptionStateActiveLicenseProductLicenseInfo{
Tags: []string{""},
},
},
},
},
}
return nil
})
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
r.Header.Set("Authorization", "Bearer sgs_abc1228e23e789431f08cd15e9be20e69b8694c2dff701b81d16250a4a861f37")
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, true, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusForbidden, w.Code)
})
t.Run("internal mode, authenticated dev license", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
resp.Data.(*dotcom.CheckAccessTokenResponse).Dotcom = dotcom.CheckAccessTokenDotcomDotcomQuery{
ProductSubscriptionByAccessToken: dotcom.CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription{
ProductSubscriptionState: dotcom.ProductSubscriptionState{
Id: "UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==",
Uuid: "6452a8fc-e650-45a7-a0a2-357f776b3b46",
IsArchived: false,
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
},
ActiveLicense: &dotcom.ProductSubscriptionStateActiveLicenseProductLicense{
Info: &dotcom.ProductSubscriptionStateActiveLicenseProductLicenseInfo{
Tags: []string{licensing.DevTag},
},
},
},
},
}
return nil
})
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
r.Header.Set("Authorization", "Bearer sgs_abc1228e23e789431f08cd15e9be20e69b8694c2dff701b81d16250a4a861f37")
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, true, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
t.Run("internal mode, authenticated internal license", func(t *testing.T) {
cache := NewMockListingCache()
client := dotcom.NewMockClient()
client.MakeRequestFunc.SetDefaultHook(func(_ context.Context, _ *graphql.Request, resp *graphql.Response) error {
resp.Data.(*dotcom.CheckAccessTokenResponse).Dotcom = dotcom.CheckAccessTokenDotcomDotcomQuery{
ProductSubscriptionByAccessToken: dotcom.CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription{
ProductSubscriptionState: dotcom.ProductSubscriptionState{
Id: "UHJvZHVjdFN1YnNjcmlwdGlvbjoiNjQ1MmE4ZmMtZTY1MC00NWE3LWEwYTItMzU3Zjc3NmIzYjQ2Ig==",
Uuid: "6452a8fc-e650-45a7-a0a2-357f776b3b46",
IsArchived: false,
CodyGatewayAccess: dotcom.ProductSubscriptionStateCodyGatewayAccess{
CodyGatewayAccessFields: dotcom.CodyGatewayAccessFields{
Enabled: true,
ChatCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
CodeCompletionsRateLimit: &dotcom.CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit{
RateLimitFields: dotcom.RateLimitFields{
Limit: 10,
IntervalSeconds: 10,
},
},
},
},
ActiveLicense: &dotcom.ProductSubscriptionStateActiveLicenseProductLicense{
Info: &dotcom.ProductSubscriptionStateActiveLicenseProductLicenseInfo{
Tags: []string{licensing.DevTag},
},
},
},
},
}
return nil
})
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
r.Header.Set("Authorization", "Bearer sgs_abc1228e23e789431f08cd15e9be20e69b8694c2dff701b81d16250a4a861f37")
(&Authenticator{
Logger: logger,
EventLogger: events.NewStdoutLogger(logger),
Sources: actor.NewSources(productsubscription.NewSource(logger, cache, client, true, concurrencyConfig)),
}).Middleware(next).ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
})
}

View File

@ -13,13 +13,13 @@ func TestOpInQuery(t *testing.T) {
var requestReceived bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestReceived = true
assert.Equal(t, r.URL.RawQuery, "CheckAccessToken")
assert.Equal(t, r.URL.RawQuery, "CheckDotcomUserAccessToken")
}))
defer srv.Close()
c := NewClient(srv.URL, "test-token", "random", "dev")
// We don't care about the actual result of the call
_, _ = CheckAccessToken(context.Background(), c, "slk_foobar")
_, _ = CheckDotcomUserAccessToken(context.Background(), c, "slk_foobar")
// But we do care that we did get through to the handler
assert.True(t, requestReceived)
}

View File

@ -10,135 +10,6 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/dotcom/genhelper"
)
// CheckAccessTokenDotcomDotcomQuery includes the requested fields of the GraphQL type DotcomQuery.
// The GraphQL type's documentation follows.
//
// Mutations that are only used on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type CheckAccessTokenDotcomDotcomQuery struct {
// The access available to the product subscription with the given access token.
// The returned ProductSubscription may be archived or not associated with an active license.
//
// Only Sourcegraph.com site admins, the account owners of the product subscription, and
// specific service accounts may perform this query.
// FOR INTERNAL USE ONLY.
ProductSubscriptionByAccessToken CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription `json:"productSubscriptionByAccessToken"`
}
// GetProductSubscriptionByAccessToken returns CheckAccessTokenDotcomDotcomQuery.ProductSubscriptionByAccessToken, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQuery) GetProductSubscriptionByAccessToken() CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription {
return v.ProductSubscriptionByAccessToken
}
// CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription includes the requested fields of the GraphQL type ProductSubscription.
// The GraphQL type's documentation follows.
//
// A product subscription that was created on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription struct {
ProductSubscriptionState `json:"-"`
}
// GetId returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.Id, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetId() string {
return v.ProductSubscriptionState.Id
}
// GetUuid returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.Uuid, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetUuid() string {
return v.ProductSubscriptionState.Uuid
}
// GetAccount returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.Account, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetAccount() *ProductSubscriptionStateAccountUser {
return v.ProductSubscriptionState.Account
}
// GetIsArchived returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.IsArchived, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetIsArchived() bool {
return v.ProductSubscriptionState.IsArchived
}
// GetCodyGatewayAccess returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.CodyGatewayAccess, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetCodyGatewayAccess() ProductSubscriptionStateCodyGatewayAccess {
return v.ProductSubscriptionState.CodyGatewayAccess
}
// GetActiveLicense returns CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription.ActiveLicense, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) GetActiveLicense() *ProductSubscriptionStateActiveLicenseProductLicense {
return v.ProductSubscriptionState.ActiveLicense
}
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription
graphql.NoUnmarshalJSON
}
firstPass.CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(
b, &v.ProductSubscriptionState)
if err != nil {
return err
}
return nil
}
type __premarshalCheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription struct {
Id string `json:"id"`
Uuid string `json:"uuid"`
Account *ProductSubscriptionStateAccountUser `json:"account"`
IsArchived bool `json:"isArchived"`
CodyGatewayAccess ProductSubscriptionStateCodyGatewayAccess `json:"codyGatewayAccess"`
ActiveLicense *ProductSubscriptionStateActiveLicenseProductLicense `json:"activeLicense"`
}
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *CheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription) __premarshalJSON() (*__premarshalCheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription, error) {
var retval __premarshalCheckAccessTokenDotcomDotcomQueryProductSubscriptionByAccessTokenProductSubscription
retval.Id = v.ProductSubscriptionState.Id
retval.Uuid = v.ProductSubscriptionState.Uuid
retval.Account = v.ProductSubscriptionState.Account
retval.IsArchived = v.ProductSubscriptionState.IsArchived
retval.CodyGatewayAccess = v.ProductSubscriptionState.CodyGatewayAccess
retval.ActiveLicense = v.ProductSubscriptionState.ActiveLicense
return &retval, nil
}
// CheckAccessTokenResponse is returned by CheckAccessToken on success.
type CheckAccessTokenResponse struct {
// Queries that are only used on Sourcegraph.com.
//
// FOR INTERNAL USE ONLY.
Dotcom CheckAccessTokenDotcomDotcomQuery `json:"dotcom"`
}
// GetDotcom returns CheckAccessTokenResponse.Dotcom, and is useful for accessing the field via an interface.
func (v *CheckAccessTokenResponse) GetDotcom() CheckAccessTokenDotcomDotcomQuery { return v.Dotcom }
// CheckDotcomUserAccessTokenDotcomDotcomQuery includes the requested fields of the GraphQL type DotcomQuery.
// The GraphQL type's documentation follows.
//
@ -642,463 +513,6 @@ func (v *DotcomUserStateCodyGatewayAccess) __premarshalJSON() (*__premarshalDotc
return &retval, nil
}
// ListProductSubscriptionFields includes the GraphQL fields of ProductSubscription requested by the fragment ListProductSubscriptionFields.
// The GraphQL type's documentation follows.
//
// A product subscription that was created on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type ListProductSubscriptionFields struct {
ProductSubscriptionState `json:"-"`
// Available access tokens for authenticating as the subscription holder with managed
// Sourcegraph services.
SourcegraphAccessTokens []string `json:"sourcegraphAccessTokens"`
}
// GetSourcegraphAccessTokens returns ListProductSubscriptionFields.SourcegraphAccessTokens, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetSourcegraphAccessTokens() []string {
return v.SourcegraphAccessTokens
}
// GetId returns ListProductSubscriptionFields.Id, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetId() string { return v.ProductSubscriptionState.Id }
// GetUuid returns ListProductSubscriptionFields.Uuid, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetUuid() string { return v.ProductSubscriptionState.Uuid }
// GetAccount returns ListProductSubscriptionFields.Account, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetAccount() *ProductSubscriptionStateAccountUser {
return v.ProductSubscriptionState.Account
}
// GetIsArchived returns ListProductSubscriptionFields.IsArchived, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetIsArchived() bool {
return v.ProductSubscriptionState.IsArchived
}
// GetCodyGatewayAccess returns ListProductSubscriptionFields.CodyGatewayAccess, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetCodyGatewayAccess() ProductSubscriptionStateCodyGatewayAccess {
return v.ProductSubscriptionState.CodyGatewayAccess
}
// GetActiveLicense returns ListProductSubscriptionFields.ActiveLicense, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionFields) GetActiveLicense() *ProductSubscriptionStateActiveLicenseProductLicense {
return v.ProductSubscriptionState.ActiveLicense
}
func (v *ListProductSubscriptionFields) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*ListProductSubscriptionFields
graphql.NoUnmarshalJSON
}
firstPass.ListProductSubscriptionFields = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(
b, &v.ProductSubscriptionState)
if err != nil {
return err
}
return nil
}
type __premarshalListProductSubscriptionFields struct {
SourcegraphAccessTokens []string `json:"sourcegraphAccessTokens"`
Id string `json:"id"`
Uuid string `json:"uuid"`
Account *ProductSubscriptionStateAccountUser `json:"account"`
IsArchived bool `json:"isArchived"`
CodyGatewayAccess ProductSubscriptionStateCodyGatewayAccess `json:"codyGatewayAccess"`
ActiveLicense *ProductSubscriptionStateActiveLicenseProductLicense `json:"activeLicense"`
}
func (v *ListProductSubscriptionFields) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *ListProductSubscriptionFields) __premarshalJSON() (*__premarshalListProductSubscriptionFields, error) {
var retval __premarshalListProductSubscriptionFields
retval.SourcegraphAccessTokens = v.SourcegraphAccessTokens
retval.Id = v.ProductSubscriptionState.Id
retval.Uuid = v.ProductSubscriptionState.Uuid
retval.Account = v.ProductSubscriptionState.Account
retval.IsArchived = v.ProductSubscriptionState.IsArchived
retval.CodyGatewayAccess = v.ProductSubscriptionState.CodyGatewayAccess
retval.ActiveLicense = v.ProductSubscriptionState.ActiveLicense
return &retval, nil
}
// ListProductSubscriptionsDotcomDotcomQuery includes the requested fields of the GraphQL type DotcomQuery.
// The GraphQL type's documentation follows.
//
// Mutations that are only used on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type ListProductSubscriptionsDotcomDotcomQuery struct {
// A list of product subscriptions.
// FOR INTERNAL USE ONLY.
ProductSubscriptions ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection `json:"productSubscriptions"`
}
// GetProductSubscriptions returns ListProductSubscriptionsDotcomDotcomQuery.ProductSubscriptions, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQuery) GetProductSubscriptions() ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection {
return v.ProductSubscriptions
}
// ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection includes the requested fields of the GraphQL type ProductSubscriptionConnection.
// The GraphQL type's documentation follows.
//
// A list of product subscriptions.
// FOR INTERNAL USE ONLY.
type ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection struct {
// The total count of product subscriptions in the connection. This total count may be larger than the number of
// nodes in this object when the result is paginated.
TotalCount int `json:"totalCount"`
// Pagination information.
PageInfo ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo `json:"pageInfo"`
// A list of product subscriptions.
Nodes []ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription `json:"nodes"`
}
// GetTotalCount returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection.TotalCount, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection) GetTotalCount() int {
return v.TotalCount
}
// GetPageInfo returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection.PageInfo, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection) GetPageInfo() ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo {
return v.PageInfo
}
// GetNodes returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection.Nodes, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnection) GetNodes() []ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription {
return v.Nodes
}
// ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription includes the requested fields of the GraphQL type ProductSubscription.
// The GraphQL type's documentation follows.
//
// A product subscription that was created on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription struct {
ListProductSubscriptionFields `json:"-"`
}
// GetSourcegraphAccessTokens returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.SourcegraphAccessTokens, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetSourcegraphAccessTokens() []string {
return v.ListProductSubscriptionFields.SourcegraphAccessTokens
}
// GetId returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.Id, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetId() string {
return v.ListProductSubscriptionFields.ProductSubscriptionState.Id
}
// GetUuid returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.Uuid, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetUuid() string {
return v.ListProductSubscriptionFields.ProductSubscriptionState.Uuid
}
// GetAccount returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.Account, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetAccount() *ProductSubscriptionStateAccountUser {
return v.ListProductSubscriptionFields.ProductSubscriptionState.Account
}
// GetIsArchived returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.IsArchived, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetIsArchived() bool {
return v.ListProductSubscriptionFields.ProductSubscriptionState.IsArchived
}
// GetCodyGatewayAccess returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.CodyGatewayAccess, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetCodyGatewayAccess() ProductSubscriptionStateCodyGatewayAccess {
return v.ListProductSubscriptionFields.ProductSubscriptionState.CodyGatewayAccess
}
// GetActiveLicense returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription.ActiveLicense, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) GetActiveLicense() *ProductSubscriptionStateActiveLicenseProductLicense {
return v.ListProductSubscriptionFields.ProductSubscriptionState.ActiveLicense
}
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription
graphql.NoUnmarshalJSON
}
firstPass.ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(
b, &v.ListProductSubscriptionFields)
if err != nil {
return err
}
return nil
}
type __premarshalListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription struct {
SourcegraphAccessTokens []string `json:"sourcegraphAccessTokens"`
Id string `json:"id"`
Uuid string `json:"uuid"`
Account *ProductSubscriptionStateAccountUser `json:"account"`
IsArchived bool `json:"isArchived"`
CodyGatewayAccess ProductSubscriptionStateCodyGatewayAccess `json:"codyGatewayAccess"`
ActiveLicense *ProductSubscriptionStateActiveLicenseProductLicense `json:"activeLicense"`
}
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription) __premarshalJSON() (*__premarshalListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription, error) {
var retval __premarshalListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionNodesProductSubscription
retval.SourcegraphAccessTokens = v.ListProductSubscriptionFields.SourcegraphAccessTokens
retval.Id = v.ListProductSubscriptionFields.ProductSubscriptionState.Id
retval.Uuid = v.ListProductSubscriptionFields.ProductSubscriptionState.Uuid
retval.Account = v.ListProductSubscriptionFields.ProductSubscriptionState.Account
retval.IsArchived = v.ListProductSubscriptionFields.ProductSubscriptionState.IsArchived
retval.CodyGatewayAccess = v.ListProductSubscriptionFields.ProductSubscriptionState.CodyGatewayAccess
retval.ActiveLicense = v.ListProductSubscriptionFields.ProductSubscriptionState.ActiveLicense
return &retval, nil
}
// ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.
// The GraphQL type's documentation follows.
//
// Pagination information for forward-only pagination. See https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo.
type ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo struct {
// When paginating forwards, the cursor to continue.
EndCursor *string `json:"endCursor"`
// When paginating forwards, are there more items?
HasNextPage bool `json:"hasNextPage"`
}
// GetEndCursor returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo) GetEndCursor() *string {
return v.EndCursor
}
// GetHasNextPage returns ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsDotcomDotcomQueryProductSubscriptionsProductSubscriptionConnectionPageInfo) GetHasNextPage() bool {
return v.HasNextPage
}
// ListProductSubscriptionsResponse is returned by ListProductSubscriptions on success.
type ListProductSubscriptionsResponse struct {
// Queries that are only used on Sourcegraph.com.
//
// FOR INTERNAL USE ONLY.
Dotcom ListProductSubscriptionsDotcomDotcomQuery `json:"dotcom"`
}
// GetDotcom returns ListProductSubscriptionsResponse.Dotcom, and is useful for accessing the field via an interface.
func (v *ListProductSubscriptionsResponse) GetDotcom() ListProductSubscriptionsDotcomDotcomQuery {
return v.Dotcom
}
// ProductSubscriptionState includes the GraphQL fields of ProductSubscription requested by the fragment ProductSubscriptionState.
// The GraphQL type's documentation follows.
//
// A product subscription that was created on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type ProductSubscriptionState struct {
// The unique ID of this product subscription.
Id string `json:"id"`
// The unique UUID of this product subscription. Unlike ProductSubscription.id, this does not
// encode the type and is not a GraphQL node ID.
Uuid string `json:"uuid"`
// The user (i.e., customer) to whom this subscription is granted, or null if the account has been deleted.
Account *ProductSubscriptionStateAccountUser `json:"account"`
// Whether this product subscription was archived.
IsArchived bool `json:"isArchived"`
// Cody Gateway access granted to this subscription. Properties may be inferred from the active license, or be defined in overrides.
CodyGatewayAccess ProductSubscriptionStateCodyGatewayAccess `json:"codyGatewayAccess"`
// The currently active product license associated with this product subscription, if any.
ActiveLicense *ProductSubscriptionStateActiveLicenseProductLicense `json:"activeLicense"`
}
// GetId returns ProductSubscriptionState.Id, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetId() string { return v.Id }
// GetUuid returns ProductSubscriptionState.Uuid, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetUuid() string { return v.Uuid }
// GetAccount returns ProductSubscriptionState.Account, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetAccount() *ProductSubscriptionStateAccountUser {
return v.Account
}
// GetIsArchived returns ProductSubscriptionState.IsArchived, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetIsArchived() bool { return v.IsArchived }
// GetCodyGatewayAccess returns ProductSubscriptionState.CodyGatewayAccess, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetCodyGatewayAccess() ProductSubscriptionStateCodyGatewayAccess {
return v.CodyGatewayAccess
}
// GetActiveLicense returns ProductSubscriptionState.ActiveLicense, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionState) GetActiveLicense() *ProductSubscriptionStateActiveLicenseProductLicense {
return v.ActiveLicense
}
// ProductSubscriptionStateAccountUser includes the requested fields of the GraphQL type User.
// The GraphQL type's documentation follows.
//
// A user.
type ProductSubscriptionStateAccountUser struct {
// The user's username.
Username string `json:"username"`
}
// GetUsername returns ProductSubscriptionStateAccountUser.Username, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateAccountUser) GetUsername() string { return v.Username }
// ProductSubscriptionStateActiveLicenseProductLicense includes the requested fields of the GraphQL type ProductLicense.
// The GraphQL type's documentation follows.
//
// A product license that was created on Sourcegraph.com.
// FOR INTERNAL USE ONLY.
type ProductSubscriptionStateActiveLicenseProductLicense struct {
// Information about this product license.
Info *ProductSubscriptionStateActiveLicenseProductLicenseInfo `json:"info"`
}
// GetInfo returns ProductSubscriptionStateActiveLicenseProductLicense.Info, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateActiveLicenseProductLicense) GetInfo() *ProductSubscriptionStateActiveLicenseProductLicenseInfo {
return v.Info
}
// ProductSubscriptionStateActiveLicenseProductLicenseInfo includes the requested fields of the GraphQL type ProductLicenseInfo.
// The GraphQL type's documentation follows.
//
// Information about this site's product license (which activates certain Sourcegraph features).
type ProductSubscriptionStateActiveLicenseProductLicenseInfo struct {
// Tags indicating the product plan and features activated by this license.
Tags []string `json:"tags"`
}
// GetTags returns ProductSubscriptionStateActiveLicenseProductLicenseInfo.Tags, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateActiveLicenseProductLicenseInfo) GetTags() []string { return v.Tags }
// ProductSubscriptionStateCodyGatewayAccess includes the requested fields of the GraphQL type CodyGatewayAccess.
// The GraphQL type's documentation follows.
//
// Cody Gateway access granted to a subscription.
// FOR INTERNAL USE ONLY.
type ProductSubscriptionStateCodyGatewayAccess struct {
CodyGatewayAccessFields `json:"-"`
}
// GetEnabled returns ProductSubscriptionStateCodyGatewayAccess.Enabled, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateCodyGatewayAccess) GetEnabled() bool {
return v.CodyGatewayAccessFields.Enabled
}
// GetChatCompletionsRateLimit returns ProductSubscriptionStateCodyGatewayAccess.ChatCompletionsRateLimit, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateCodyGatewayAccess) GetChatCompletionsRateLimit() *CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit {
return v.CodyGatewayAccessFields.ChatCompletionsRateLimit
}
// GetCodeCompletionsRateLimit returns ProductSubscriptionStateCodyGatewayAccess.CodeCompletionsRateLimit, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateCodyGatewayAccess) GetCodeCompletionsRateLimit() *CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit {
return v.CodyGatewayAccessFields.CodeCompletionsRateLimit
}
// GetEmbeddingsRateLimit returns ProductSubscriptionStateCodyGatewayAccess.EmbeddingsRateLimit, and is useful for accessing the field via an interface.
func (v *ProductSubscriptionStateCodyGatewayAccess) GetEmbeddingsRateLimit() *CodyGatewayAccessFieldsEmbeddingsRateLimitCodyGatewayRateLimit {
return v.CodyGatewayAccessFields.EmbeddingsRateLimit
}
func (v *ProductSubscriptionStateCodyGatewayAccess) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
var firstPass struct {
*ProductSubscriptionStateCodyGatewayAccess
graphql.NoUnmarshalJSON
}
firstPass.ProductSubscriptionStateCodyGatewayAccess = v
err := json.Unmarshal(b, &firstPass)
if err != nil {
return err
}
err = json.Unmarshal(
b, &v.CodyGatewayAccessFields)
if err != nil {
return err
}
return nil
}
type __premarshalProductSubscriptionStateCodyGatewayAccess struct {
Enabled bool `json:"enabled"`
ChatCompletionsRateLimit *CodyGatewayAccessFieldsChatCompletionsRateLimitCodyGatewayRateLimit `json:"chatCompletionsRateLimit"`
CodeCompletionsRateLimit *CodyGatewayAccessFieldsCodeCompletionsRateLimitCodyGatewayRateLimit `json:"codeCompletionsRateLimit"`
EmbeddingsRateLimit *CodyGatewayAccessFieldsEmbeddingsRateLimitCodyGatewayRateLimit `json:"embeddingsRateLimit"`
}
func (v *ProductSubscriptionStateCodyGatewayAccess) MarshalJSON() ([]byte, error) {
premarshaled, err := v.__premarshalJSON()
if err != nil {
return nil, err
}
return json.Marshal(premarshaled)
}
func (v *ProductSubscriptionStateCodyGatewayAccess) __premarshalJSON() (*__premarshalProductSubscriptionStateCodyGatewayAccess, error) {
var retval __premarshalProductSubscriptionStateCodyGatewayAccess
retval.Enabled = v.CodyGatewayAccessFields.Enabled
retval.ChatCompletionsRateLimit = v.CodyGatewayAccessFields.ChatCompletionsRateLimit
retval.CodeCompletionsRateLimit = v.CodyGatewayAccessFields.CodeCompletionsRateLimit
retval.EmbeddingsRateLimit = v.CodyGatewayAccessFields.EmbeddingsRateLimit
return &retval, nil
}
// RateLimitFields includes the GraphQL fields of CodyGatewayRateLimit requested by the fragment RateLimitFields.
// The GraphQL type's documentation follows.
//
@ -1203,14 +617,6 @@ func (v *SnippetAttributionSnippetAttributionSnippetAttributionConnectionNodesSn
return v.RepositoryName
}
// __CheckAccessTokenInput is used internally by genqlient
type __CheckAccessTokenInput struct {
Token string `json:"token"`
}
// GetToken returns __CheckAccessTokenInput.Token, and is useful for accessing the field via an interface.
func (v *__CheckAccessTokenInput) GetToken() string { return v.Token }
// __CheckDotcomUserAccessTokenInput is used internally by genqlient
type __CheckDotcomUserAccessTokenInput struct {
Token string `json:"token"`
@ -1231,76 +637,6 @@ func (v *__SnippetAttributionInput) GetSnippet() string { return v.Snippet }
// GetFirst returns __SnippetAttributionInput.First, and is useful for accessing the field via an interface.
func (v *__SnippetAttributionInput) GetFirst() int { return v.First }
// CheckAccessToken returns traits of the product subscription associated with
// the given access token.
func CheckAccessToken(
ctx context.Context,
client graphql.Client,
token string,
) (*CheckAccessTokenResponse, error) {
req := &graphql.Request{
OpName: "CheckAccessToken",
Query: `
query CheckAccessToken ($token: String!) {
dotcom {
productSubscriptionByAccessToken(accessToken: $token) {
... ProductSubscriptionState
}
}
}
fragment ProductSubscriptionState on ProductSubscription {
id
uuid
account {
username
}
isArchived
codyGatewayAccess {
... CodyGatewayAccessFields
}
activeLicense {
info {
tags
}
}
}
fragment CodyGatewayAccessFields on CodyGatewayAccess {
enabled
chatCompletionsRateLimit {
... RateLimitFields
}
codeCompletionsRateLimit {
... RateLimitFields
}
embeddingsRateLimit {
... RateLimitFields
}
}
fragment RateLimitFields on CodyGatewayRateLimit {
allowedModels
source
limit
intervalSeconds
}
`,
Variables: &__CheckAccessTokenInput{
Token: token,
},
}
var err error
var data CheckAccessTokenResponse
resp := &graphql.Response{Data: &data}
err = client.MakeRequest(
ctx,
req,
resp,
)
return &data, err
}
// CheckDotcomUserAccessToken returns traits of the product subscription associated with
// the given access token.
func CheckDotcomUserAccessToken(
@ -1362,81 +698,6 @@ fragment RateLimitFields on CodyGatewayRateLimit {
return &data, err
}
func ListProductSubscriptions(
ctx context.Context,
client graphql.Client,
) (*ListProductSubscriptionsResponse, error) {
req := &graphql.Request{
OpName: "ListProductSubscriptions",
Query: `
query ListProductSubscriptions {
dotcom {
productSubscriptions {
totalCount
pageInfo {
endCursor
hasNextPage
}
nodes {
... ListProductSubscriptionFields
}
}
}
}
fragment ListProductSubscriptionFields on ProductSubscription {
... ProductSubscriptionState
sourcegraphAccessTokens
}
fragment ProductSubscriptionState on ProductSubscription {
id
uuid
account {
username
}
isArchived
codyGatewayAccess {
... CodyGatewayAccessFields
}
activeLicense {
info {
tags
}
}
}
fragment CodyGatewayAccessFields on CodyGatewayAccess {
enabled
chatCompletionsRateLimit {
... RateLimitFields
}
codeCompletionsRateLimit {
... RateLimitFields
}
embeddingsRateLimit {
... RateLimitFields
}
}
fragment RateLimitFields on CodyGatewayRateLimit {
allowedModels
source
limit
intervalSeconds
}
`,
}
var err error
var data ListProductSubscriptionsResponse
resp := &graphql.Response{Data: &data}
err = client.MakeRequest(
ctx,
req,
resp,
)
return &data, err
}
// Searches the instances indexed code for code matching snippet.
func SnippetAttribution(
ctx context.Context,

View File

@ -18,53 +18,6 @@ fragment CodyGatewayAccessFields on CodyGatewayAccess {
}
}
fragment ProductSubscriptionState on ProductSubscription {
id
uuid
account {
username
}
isArchived
codyGatewayAccess {
...CodyGatewayAccessFields
}
activeLicense {
info {
tags
}
}
}
# CheckAccessToken returns traits of the product subscription associated with
# the given access token.
query CheckAccessToken($token: String!) {
dotcom {
productSubscriptionByAccessToken(accessToken: $token) {
...ProductSubscriptionState
}
}
}
fragment ListProductSubscriptionFields on ProductSubscription {
...ProductSubscriptionState
sourcegraphAccessTokens
}
query ListProductSubscriptions {
dotcom {
productSubscriptions {
totalCount
pageInfo {
endCursor
hasNextPage
}
nodes {
...ListProductSubscriptionFields
}
}
}
}
fragment DotcomUserState on CodyGatewayDotcomUser {
id
username

View File

@ -35,6 +35,7 @@ go_library(
"//internal/trace",
"//internal/version",
"//lib/errors",
"//lib/pointers",
"@com_github_gorilla_mux//:mux",
"@com_github_khan_genqlient//graphql",
"@com_github_sourcegraph_log//:log",

View File

@ -27,7 +27,7 @@ func NewHandler(client graphql.Client, baseLogger log.Logger) http.Handler {
ctx := r.Context()
a := actor.FromContext(ctx)
logger := a.Logger(trace.Logger(ctx, baseLogger))
if got, want := a.GetSource(), codygateway.ActorSourceProductSubscription; got != want {
if got, want := a.GetSource(), codygateway.ActorSourceEnterpriseSubscription; got != want {
response.JSONError(logger, w, http.StatusUnauthorized, errors.New("only available for enterprise product subscriptions"))
return
}

View File

@ -95,7 +95,7 @@ func request(t *testing.T) *http.Request {
func TestSuccess(t *testing.T) {
logger := logtest.Scoped(t)
ps := fakeActorSource{
name: codygateway.ActorSourceProductSubscription,
name: codygateway.ActorSourceEnterpriseSubscription,
}
authr := &auth.Authenticator{
Sources: actor.NewSources(ps),
@ -177,7 +177,7 @@ func TestFailsForDotcomUsers(t *testing.T) {
func TestUnavailableIfConfigDisabled(t *testing.T) {
logger := logtest.Scoped(t)
dotCom := fakeActorSource{
name: codygateway.ActorSourceProductSubscription,
name: codygateway.ActorSourceEnterpriseSubscription,
}
authr := &auth.Authenticator{
Sources: actor.NewSources(dotCom),

View File

@ -16,6 +16,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/instrumentation"
"github.com/sourcegraph/sourcegraph/internal/redispool"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/pointers"
"github.com/sourcegraph/sourcegraph/internal/sams"
)
@ -29,23 +30,28 @@ const samsScopeFlaggedPromptRead = "cody_gateway::flaggedprompts::read"
func NewMaintenanceHandler(
baseLogger log.Logger, next http.Handler, config *config.Config, redisKV redispool.KeyValue) http.Handler {
// Do nothing if no SAMS configuration is provided.
if !config.SAMSClientConfig.Valid() {
baseLogger.Warn("No SAMS client config provided. Not registering maintenance endpoints.")
if err := config.SAMSClientConfig.Validate(); err != nil {
baseLogger.Warn("no SAMS client config provided; not registering maintenance endpoints",
log.Error(err))
return next
}
logger := baseLogger.Scoped("Maintenance")
// Create the SAMS Client.
samsAPIURL := pointers.Deref(
config.SAMSClientConfig.ConnConfig.APIURL,
config.SAMSClientConfig.ConnConfig.ExternalURL, // default
)
samsClient := sams.NewClient(
config.SAMSClientConfig.URL,
samsAPIURL,
clientcredentials.Config{
ClientID: config.SAMSClientConfig.ClientID,
ClientSecret: config.SAMSClientConfig.ClientSecret,
// Since we are only using our SAMS client to verify supplied token,
// we just issue tokens with a minimal set of scopes.
Scopes: []string{"openid", "profile", "email"},
TokenURL: fmt.Sprintf("%s/oauth/token", config.SAMSClientConfig.URL),
TokenURL: fmt.Sprintf("%s/oauth/token", samsAPIURL),
})
return newMaintenanceHandler(logger, next, redisKV, samsClient)

View File

@ -150,7 +150,7 @@ func handleNotify(
var actorLink string
switch actor.GetSource() {
case codygateway.ActorSourceProductSubscription:
case codygateway.ActorSourceEnterpriseSubscription:
actorLink = fmt.Sprintf("<%s/site-admin/dotcom/product/subscriptions/%s|%s>", dotcomURL, actor.GetID(), actor.GetName())
default:
actorLink = fmt.Sprintf("`%s`", actor.GetID())

View File

@ -16,13 +16,13 @@ import (
func TestThresholds(t *testing.T) {
th := Thresholds{
codygateway.ActorSourceDotcomUser: []int{100},
codygateway.ActorSourceProductSubscription: []int{100, 90},
codygateway.ActorSourceDotcomUser: []int{100},
codygateway.ActorSourceEnterpriseSubscription: []int{100, 90},
}
// Explicitly configured
autogold.Expect([]int{100}).Equal(t, th.Get(codygateway.ActorSourceDotcomUser))
// Sorted
autogold.Expect([]int{90, 100}).Equal(t, th.Get(codygateway.ActorSourceProductSubscription))
autogold.Expect([]int{90, 100}).Equal(t, th.Get(codygateway.ActorSourceEnterpriseSubscription))
// Defaults
autogold.Expect([]int{}).Equal(t, th.Get(codygateway.ActorSource("anonymous")))
}
@ -92,7 +92,7 @@ func TestSlackRateLimitNotifier(t *testing.T) {
logger,
test.mockRedis(t),
"https://sourcegraph.com/",
Thresholds{codygateway.ActorSourceProductSubscription: []int{50, 80, 90}},
Thresholds{codygateway.ActorSourceEnterpriseSubscription: []int{50, 80, 90}},
"https://hooks.slack.com",
func(ctx context.Context, url string, msg *slack.WebhookMessage) error {
alerted = true
@ -104,7 +104,7 @@ func TestSlackRateLimitNotifier(t *testing.T) {
&mockActor{
id: "foobar",
name: "alice",
source: codygateway.ActorSourceProductSubscription,
source: codygateway.ActorSourceEnterpriseSubscription,
},
codygateway.FeatureChatCompletions,
test.usageRatio,

View File

@ -16,6 +16,7 @@ go_library(
"//cmd/cody-gateway/internal/actor/anonymous",
"//cmd/cody-gateway/internal/actor/dotcomuser",
"//cmd/cody-gateway/internal/actor/productsubscription",
"//cmd/cody-gateway/internal/actor/productsubscription/enterpriseportal",
"//cmd/cody-gateway/internal/auth",
"//cmd/cody-gateway/internal/dotcom",
"//cmd/cody-gateway/internal/events",
@ -42,6 +43,8 @@ go_library(
"//internal/tracer/oteldefaults",
"//internal/tracer/oteldefaults/exporters",
"//internal/version",
"//lib/background",
"//lib/enterpriseportal/codyaccess/v1:codyaccess",
"//lib/errors",
"@com_github_go_redsync_redsync_v4//:redsync",
"@com_github_go_redsync_redsync_v4//redis/redigo",
@ -52,6 +55,8 @@ go_library(
"@com_github_slack_go_slack//:slack",
"@com_github_sourcegraph_conc//:conc",
"@com_github_sourcegraph_log//:log",
"@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go",
"@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes",
"@io_opentelemetry_go_contrib_detectors_gcp//:gcp",
"@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:otelhttp",
"@io_opentelemetry_go_otel//:otel",

View File

@ -16,5 +16,6 @@ go_library(
"//internal/env",
"//internal/trace/policy",
"//lib/errors",
"@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go",
],
)

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/httpapi/embeddings"
"github.com/sourcegraph/sourcegraph/internal/codygateway"
"github.com/sourcegraph/sourcegraph/internal/collections"
@ -30,7 +31,6 @@ type Config struct {
Dotcom struct {
URL string
AccessToken string
InternalMode bool
ActorRefreshCoolDownInterval time.Duration
// Prompts that get flagged are stored in Redis for a short-time, for
@ -40,6 +40,10 @@ type Config struct {
ClientID string
}
EnterprisePortal struct {
URL *url.URL
}
Anthropic AnthropicConfig
OpenAI OpenAIConfig
@ -162,13 +166,19 @@ type FlaggingConfig struct {
}
type SAMSClientConfig struct {
URL string
ConnConfig sams.ConnConfig
ClientID string
ClientSecret string
}
func (scc SAMSClientConfig) Valid() bool {
return !(scc.URL == "" || scc.ClientID == "" || scc.ClientSecret == "")
func (sams SAMSClientConfig) Validate() error {
if err := sams.ConnConfig.Validate(); err != nil {
return errors.Wrap(err, "invalid ConnConfig")
}
if sams.ClientID == "" || sams.ClientSecret == "" {
return errors.New("ClientID and ClientSecret must be set")
}
return nil
}
func (c *Config) Load() {
@ -179,20 +189,28 @@ func (c *Config) Load() {
"should be used as 'Authorization: Bearer $secret' header when accessing diagnostics endpoints.")
c.Dotcom.AccessToken = c.GetOptional("CODY_GATEWAY_DOTCOM_ACCESS_TOKEN",
"The Sourcegraph.com access token to be used. If not provided, dotcom-based actor sources will be disabled.")
"The Sourcegraph.com access token to be used. If not provided, the dotcom-user actor source will be disabled.")
c.Dotcom.ClientID = c.GetOptional("CODY_GATEWAY_DOTCOM_CLIENT_ID",
"Value of X-Sourcegraph-Client-Id header to be passed to sourcegraph.com.")
c.Dotcom.URL = c.Get("CODY_GATEWAY_DOTCOM_API_URL", "https://sourcegraph.com/.api/graphql", "Custom override for the dotcom API endpoint")
if _, err := url.Parse(c.Dotcom.URL); err != nil {
c.AddError(errors.Wrap(err, "invalid CODY_GATEWAY_DOTCOM_API_URL"))
}
c.Dotcom.InternalMode = c.GetBool("CODY_GATEWAY_DOTCOM_INTERNAL_MODE", "false", "Only allow tokens associated with active internal and dev licenses to be used.") ||
c.GetBool("CODY_GATEWAY_DOTCOM_DEV_LICENSES_ONLY", "false", "DEPRECATED, use CODY_GATEWAY_DOTCOM_INTERNAL_MODE")
c.Dotcom.ActorRefreshCoolDownInterval = c.GetInterval("CODY_GATEWAY_DOTCOM_ACTOR_COOLDOWN_INTERVAL", "300s",
"Cooldown period for refreshing the actor info from dotcom.")
c.Dotcom.FlaggedPromptRecorderTTL = c.GetInterval("CODY_GATEWAY_DOTCOM_FLAGGED_PROMPT_RECORDER_TTL", "1h",
"Period to retain prompts in Redis.")
enterprisePortalURL := c.GetOptional("CODY_GATEWAY_ENTERPRISE_PORTAL_URL",
"The Enterprise Portal instance to target. If not provided, the product subscriptions actor source will be disabled.")
if enterprisePortalURL != "" {
var err error
c.EnterprisePortal.URL, err = url.Parse(enterprisePortalURL)
if err != nil {
c.AddError(errors.Wrap(err, "invalid CODY_GATEWAY_ENTERPRISE_PORTAL_URL"))
}
}
c.Anthropic.AccessToken = c.Get("CODY_GATEWAY_ANTHROPIC_ACCESS_TOKEN", "", "The Anthropic access token to be used.")
c.Anthropic.AllowedModels = splitMaybe(c.Get("CODY_GATEWAY_ANTHROPIC_ALLOWED_MODELS",
strings.Join([]string{
@ -343,7 +361,12 @@ func (c *Config) Load() {
c.Sourcegraph.EmbeddingsAPIURL = c.Get("CODY_GATEWAY_SOURCEGRAPH_EMBEDDINGS_API_URL", "https://embeddings.sourcegraph.com/v2/models/st-multi-qa-mpnet-base-dot-v1/infer", "URL of the SMEGA API.")
c.Sourcegraph.EmbeddingsAPIToken = c.Get("CODY_GATEWAY_SOURCEGRAPH_EMBEDDINGS_API_TOKEN", "", "Token to use for the SMEGA API.")
c.SAMSClientConfig.URL = c.GetOptional("SAMS_URL", "SAMS service endpoint")
// SAMS_URL, SAMS_API_URL are same keys used for sams.NewConnConfigFromEnv
c.SAMSClientConfig.ConnConfig.ExternalURL = c.Get("SAMS_URL", "https://accounts.sourcegraph.com",
"SAMS external service endpoint")
if apiurl := c.GetOptional("SAMS_API_URL", "SAMS API endpoint"); apiurl != "" {
c.SAMSClientConfig.ConnConfig.APIURL = &apiurl
}
c.SAMSClientConfig.ClientID = c.GetOptional("SAMS_CLIENT_ID", "SAMS OAuth client ID")
c.SAMSClientConfig.ClientSecret = c.GetOptional("SAMS_CLIENT_SECRET", "SAMS OAuth client secret")

View File

@ -23,14 +23,9 @@ import (
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/shared/config"
sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
"github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/auth"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/events"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/httpapi/completions"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/httpapi/featurelimiter"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/limiter"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/notify"
"github.com/sourcegraph/sourcegraph/internal/codygateway"
"github.com/sourcegraph/sourcegraph/internal/goroutine"
"github.com/sourcegraph/sourcegraph/internal/httpcli"
@ -43,14 +38,25 @@ import (
"github.com/sourcegraph/sourcegraph/internal/service"
"github.com/sourcegraph/sourcegraph/internal/trace"
"github.com/sourcegraph/sourcegraph/internal/version"
"github.com/sourcegraph/sourcegraph/lib/background"
codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/anonymous"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/dotcomuser"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription/enterpriseportal"
"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/cmd/cody-gateway/internal/httpapi/completions"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/httpapi/featurelimiter"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/limiter"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/notify"
"github.com/sourcegraph/sourcegraph/cmd/cody-gateway/shared/config"
)
func Main(ctx context.Context, obctx *observation.Context, ready service.ReadyFunc, cfg *config.Config) error {
@ -125,7 +131,7 @@ func Main(ctx context.Context, obctx *observation.Context, ready service.ReadyFu
dotcomURL.String(),
notify.Thresholds{
// Detailed notifications for product subscriptions.
codygateway.ActorSourceProductSubscription: []int{90, 95, 100},
codygateway.ActorSourceEnterpriseSubscription: []int{90, 95, 100},
// No notifications for individual dotcom users - this can get quite
// spammy.
codygateway.ActorSourceDotcomUser: []int{},
@ -138,24 +144,63 @@ func Main(ctx context.Context, obctx *observation.Context, ready service.ReadyFu
// Supported actor/auth sources
sources := actor.NewSources(anonymous.NewSource(cfg.AllowAnonymous, cfg.ActorConcurrencyLimit))
var dotcomClient graphql.Client
if cfg.Dotcom.AccessToken != "" {
// dotcom-based actor sources only if an access token is provided for
// us to talk with the client
obctx.Logger.Info("dotcom-based actor sources are enabled")
obctx.Logger.Info("dotcom-user actor source enabled")
dotcomClient = dotcom.NewClient(cfg.Dotcom.URL, cfg.Dotcom.AccessToken, cfg.Dotcom.ClientID, cfg.Environment)
sources.Add(
dotcomuser.NewSource(
obctx.Logger,
rcache.NewWithTTL(fmt.Sprintf("dotcom-users:%s", dotcomuser.SourceVersion), int(cfg.SourcesCacheTTL.Seconds())),
dotcomClient,
cfg.ActorConcurrencyLimit,
rs,
cfg.Dotcom.ActorRefreshCoolDownInterval,
),
)
} else {
obctx.Logger.Error("CODY_GATEWAY_DOTCOM_ACCESS_TOKEN is not set, dotcom-user actor source is disabled")
}
// backgroundRoutines collects background dependencies for shutdown
var backgroundRoutines []background.Routine
if cfg.EnterprisePortal.URL != nil {
obctx.Logger.Info("enterprise subscriptions actor source enabled")
conn, err := enterpriseportal.Dial(
ctx,
obctx.Logger.Scoped("enterpriseportal"),
cfg.EnterprisePortal.URL,
// Authenticate using SAMS client credentials
sams.ClientCredentialsTokenSource(
cfg.SAMSClientConfig.ConnConfig,
cfg.SAMSClientConfig.ClientID,
cfg.SAMSClientConfig.ClientSecret,
[]scopes.Scope{
scopes.ToScope(scopes.ServiceEnterprisePortal, "codyaccess", scopes.ActionRead),
scopes.ToScope(scopes.ServiceEnterprisePortal, "subscription", scopes.ActionRead),
},
),
)
if err != nil {
return errors.Wrap(err, "connect to Enterprise Portal")
}
backgroundRoutines = append(backgroundRoutines, background.CallbackRoutine{
NameFunc: func() string { return "enterpriseportal.grpc.conn" },
StopFunc: func(context.Context) error { return conn.Close() },
})
sources.Add(
productsubscription.NewSource(
obctx.Logger,
rcache.NewWithTTL(fmt.Sprintf("product-subscriptions:%s", productsubscription.SourceVersion), int(cfg.SourcesCacheTTL.Seconds())),
dotcomClient,
cfg.Dotcom.InternalMode,
codyaccessv1.NewCodyAccessServiceClient(conn),
cfg.ActorConcurrencyLimit,
),
dotcomuser.NewSource(obctx.Logger, rcache.NewWithTTL(fmt.Sprintf("dotcom-users:%s", dotcomuser.SourceVersion), int(cfg.SourcesCacheTTL.Seconds())), dotcomClient, cfg.ActorConcurrencyLimit, rs, cfg.Dotcom.ActorRefreshCoolDownInterval),
)
} else {
obctx.Logger.Warn("CODY_GATEWAY_DOTCOM_ACCESS_TOKEN is not set, dotcom-based actor sources are disabled")
obctx.Logger.Error("CODY_GATEWAY_ENTERPRISE_PORTAL_URL is not set, enterprise subscriptions actor source is disabled")
}
authr := &auth.Authenticator{
@ -227,17 +272,23 @@ func Main(ctx context.Context, obctx *observation.Context, ready service.ReadyFu
ready()
obctx.Logger.Info("service ready", log.String("address", address))
// Collect background routines
backgroundRoutines := []goroutine.BackgroundRoutine{
// Collect service routines
serviceRoutines := []goroutine.BackgroundRoutine{
server,
sources.Worker(obctx, sourceWorkerMutex, cfg.SourcesSyncInterval),
}
if w, ok := eventLogger.(goroutine.BackgroundRoutine); ok {
// eventLogger is events.BufferedLogger
// eventLogger is events.BufferedLogger, we need to shut it down cleanly
backgroundRoutines = append(backgroundRoutines, w)
}
// Block until done
return goroutine.MonitorBackgroundRoutines(ctx, backgroundRoutines...)
return goroutine.MonitorBackgroundRoutines(ctx, background.FIFOSTopRoutine{
// Stop important service routines first
background.CombinedRoutine(serviceRoutines),
// Then shut down other background services
background.CombinedRoutine(backgroundRoutines),
})
}
func newRedisStore(store redispool.KeyValue) limiter.RedisStore {

View File

@ -86,7 +86,7 @@ func (s *handlerV1) GetCodyGatewayAccess(ctx context.Context, req *connect.Reque
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid query"))
}
if err != nil {
if err == dotcomdb.ErrCodyGatewayAccessNotFound {
if errors.Is(err, dotcomdb.ErrCodyGatewayAccessNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connectutil.InternalError(ctx, logger, err,

View File

@ -67,7 +67,7 @@ func (r codyGatewayAccessResolver) ChatCompletionsRateLimit(ctx context.Context)
return &codyGatewayRateLimitResolver{
feature: types.CompletionsFeatureChat,
actorID: r.sub.UUID(),
actorSource: codygateway.ActorSourceProductSubscription,
actorSource: codygateway.ActorSourceEnterpriseSubscription,
v: rateLimit,
source: source,
}, nil
@ -107,7 +107,7 @@ func (r codyGatewayAccessResolver) CodeCompletionsRateLimit(ctx context.Context)
return &codyGatewayRateLimitResolver{
feature: types.CompletionsFeatureCode,
actorID: r.sub.UUID(),
actorSource: codygateway.ActorSourceProductSubscription,
actorSource: codygateway.ActorSourceEnterpriseSubscription,
v: rateLimit,
source: source,
}, nil
@ -146,7 +146,7 @@ func (r codyGatewayAccessResolver) EmbeddingsRateLimit(ctx context.Context) (gra
return &codyGatewayRateLimitResolver{
actorID: r.sub.UUID(),
actorSource: codygateway.ActorSourceProductSubscription,
actorSource: codygateway.ActorSourceEnterpriseSubscription,
v: rateLimit,
source: source,
}, nil

View File

@ -124,5 +124,6 @@ write_source_files(
"//cmd/repo-updater/internal/gitserver:generate_mocks",
"//dev/build-tracker:generate_mocks",
"//cmd/worker/internal/sourcegraphaccounts:generate_mocks",
"//cmd/cody-gateway/internal/actor/productsubscription/productsubscriptiontest:generate_mocks",
],
)

View File

@ -3,8 +3,12 @@ package codygateway
type ActorSource string
const (
ActorSourceProductSubscription ActorSource = "dotcom-product-subscriptions"
ActorSourceDotcomUser ActorSource = "dotcom-user"
// We retain legacy naming just in case there are hard dependencies on this
// name. Today, these are Enterprise Subscriptions sourced from the Enterprise
// Portal service.
ActorSourceEnterpriseSubscription ActorSource = "dotcom-product-subscriptions"
// Sourcegraph.com user actors.
ActorSourceDotcomUser ActorSource = "dotcom-user"
)
const CompletionsEventFeatureMetadataField = "feature"

View File

@ -0,0 +1,14 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "grpcoauth",
srcs = ["grpcoauth.go"],
importpath = "github.com/sourcegraph/sourcegraph/internal/grpc/grpcoauth",
visibility = ["//:__subpackages__"],
deps = [
"//internal/env",
"//lib/errors",
"@org_golang_google_grpc//credentials",
"@org_golang_x_oauth2//:oauth2",
],
)

View File

@ -0,0 +1,43 @@
package grpcoauth
import (
"context"
"golang.org/x/oauth2"
"google.golang.org/grpc/credentials"
"github.com/sourcegraph/sourcegraph/internal/env"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// TokenSource supplies PerRPCCredentials from an oauth2.TokenSource.
// It is a fork of the implementation in "google.golang.org/grpc/credentials/oauth",
// but checks "internal/env" to toggle the "must require secure transport"
// behaviour. Without these changes, the upstream token source cannot be used
// in local development.
type TokenSource struct {
oauth2.TokenSource
}
// GetRequestMetadata gets the request metadata as a map from a TokenSource.
func (ts TokenSource) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
token, err := ts.Token()
if err != nil {
return nil, err
}
if !env.InsecureDev {
ri, _ := credentials.RequestInfoFromContext(ctx)
if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
return nil, errors.Newf("unable to transfer TokenSource PerRPCCredentials: %w", err)
}
}
return map[string]string{
"authorization": token.Type() + " " + token.AccessToken,
}, nil
}
// RequireTransportSecurity indicates whether the credentials requires transport security.
// For this implementation, we disable it if the INSECURE_DEV environment variable is set.
func (ts TokenSource) RequireTransportSecurity() bool {
return !env.InsecureDev
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"os/signal"
"slices"
"sync"
"syscall"
@ -152,7 +153,25 @@ func (rs CombinedRoutine) Stop(ctx context.Context) error {
}
// LIFOStopRoutine is a list of routines which are started in unison, but stopped
// sequentially last-in-first-out (the last Routine is stopped, and once it
// sequentially first-in-first-out order (the first Routine is stopped, and once
// it successfully stops, the next routine is stopped).
//
// This is useful for services where subprocessors should be stopped before the
// primary service stops for a graceful shutdown.
type FIFOSTopRoutine []Routine
func (r FIFOSTopRoutine) Name() string { return "fifo" }
func (r FIFOSTopRoutine) Start() { CombinedRoutine(r).Start() }
func (r FIFOSTopRoutine) Stop(ctx context.Context) error {
// Pass self inverted into LIFOStopRoutine
slices.Reverse(r)
return LIFOStopRoutine(r).Stop(ctx)
}
// LIFOStopRoutine is a list of routines which are started in unison, but stopped
// sequentially last-in-first-out order (the last Routine is stopped, and once it
// successfully stops, the next routine is stopped).
//
// This is useful for services where subprocessors should be stopped before the

View File

@ -166,3 +166,29 @@ func TestLIFOStopRoutine(t *testing.T) {
// stops in reverse
assert.Equal(t, []string{"r3", "r2", "r1"}, stopped)
}
func TestFIFOStopRoutine(t *testing.T) {
// use an unguarded slice because FIFOSTopRoutine should only stop in sequence
var stopped []string
r1 := NewMockRoutine()
r1.StopFunc.PushHook(func(context.Context) error {
stopped = append(stopped, "r1")
return nil
})
r2 := NewMockRoutine()
r2.StopFunc.PushHook(func(context.Context) error {
stopped = append(stopped, "r2")
return nil
})
r3 := NewMockRoutine()
r3.StopFunc.PushHook(func(context.Context) error {
stopped = append(stopped, "r3")
return nil
})
r := FIFOSTopRoutine{r1, r2, r3}
err := r.Stop(context.Background())
require.NoError(t, err)
// stops in order
assert.Equal(t, []string{"r1", "r2", "r3"}, stopped)
}

View File

@ -0,0 +1,8 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "enterpriseportal",
srcs = ["enterpriseportal.go"],
importpath = "github.com/sourcegraph/sourcegraph/lib/enterpriseportal",
visibility = ["//visibility:public"],
)

View File

@ -12,4 +12,5 @@ sg gen buf \
lib/enterpriseportal/codyaccess/v1/buf.gen.yaml
```
> **EVERYTHING HERE IS IN A DRAFT STATE** - see [RFC 885](https://docs.google.com/document/d/1tiaW1IVKm_YSSYhH-z7Q8sv4HSO_YJ_Uu6eYDjX7uU4/edit#heading=h.tdaxc5h34u7q).
> [!CAUTION]
> These APIs have **production dependents**. Make changes with extreme care for backwards-compatibility.

View File

@ -0,0 +1 @@
package enterpriseportal

View File

@ -432,3 +432,10 @@
package: main
interfaces:
- BigQueryWriter
- filename: cmd/cody-gateway/internal/actor/productsubscription/productsubscriptiontest/mocks.go
package: productsubscriptiontest
sources:
- path: github.com/sourcegraph/sourcegraph/cmd/cody-gateway/internal/actor/productsubscription
interfaces:
- EnterprisePortalClient

View File

@ -317,6 +317,17 @@ commands:
CODY_GATEWAY_FIREWORKS_ACCESS_TOKEN: sekret
CODY_GATEWAY_SOURCEGRAPH_EMBEDDINGS_API_TOKEN: sekret
CODY_GATEWAY_GOOGLE_ACCESS_TOKEN: sekret
# Connect to services that require SAMS M2M http://go/sams-m2m
SAMS_URL: https://accounts.sgdev.org
# Connect to Enterprise Portal running locally
CODY_GATEWAY_ENTERPRISE_PORTAL_URL: http://localhost:6081
externalSecrets:
SAMS_CLIENT_ID:
project: sourcegraph-local-dev
name: SG_LOCAL_DEV_SAMS_CLIENT_ID
SAMS_CLIENT_SECRET:
project: sourcegraph-local-dev
name: SG_LOCAL_DEV_SAMS_CLIENT_SECRET
watch:
- lib
- internal
@ -422,7 +433,6 @@ commands:
ENTERPRISE_PORTAL_SAMS_CLIENT_SECRET:
project: sourcegraph-local-dev
name: SG_LOCAL_DEV_SAMS_CLIENT_SECRET
watch:
- lib
- cmd/enterprise-portal
@ -1052,6 +1062,18 @@ bazelCommands:
CODY_GATEWAY_FIREWORKS_ACCESS_TOKEN: sekret
CODY_GATEWAY_SOURCEGRAPH_EMBEDDINGS_API_TOKEN: sekret
CODY_GATEWAY_GOOGLE_ACCESS_TOKEN: sekret
# Connect to services that require SAMS M2M http://go/sams-m2m
SAMS_URL: https://accounts.sgdev.org
# Connect to Enterprise Portal running locally
CODY_GATEWAY_ENTERPRISE_PORTAL_URL: http://localhost:6081
externalSecrets:
SAMS_CLIENT_ID:
project: sourcegraph-local-dev
name: SG_LOCAL_DEV_SAMS_CLIENT_ID
SAMS_CLIENT_SECRET:
project: sourcegraph-local-dev
name: SG_LOCAL_DEV_SAMS_CLIENT_SECRET
docsite:
runTarget: //doc:serve
searcher:
@ -1394,6 +1416,7 @@ commandsets:
- blobstore
- embeddings
- cody-gateway
- enterprise-portal # required by Cody Gateway
env:
SOURCEGRAPHDOTCOM_MODE: true