feat/enterpriseportal: add license creation webhook (#64422)

Closes https://linear.app/sourcegraph/issue/CORE-241
Originally added in
https://github.com/sourcegraph/sourcegraph/pull/59214

This seems unused, but might be good to have for completedness-sake

## Test plan

Unit tests
This commit is contained in:
Robert Lin 2024-08-13 12:11:03 -07:00 committed by GitHub
parent 2649987bac
commit 043e780590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 373 additions and 2 deletions

View File

@ -22,11 +22,13 @@ go_library(
"//internal/collections",
"//internal/license",
"//internal/licensing",
"//internal/slack",
"//internal/trace",
"//lib/enterpriseportal/subscriptions/v1:subscriptions",
"//lib/enterpriseportal/subscriptions/v1/v1connect",
"//lib/errors",
"//lib/managedservicesplatform/iam",
"//lib/managedservicesplatform/runtime/contract",
"//lib/pointers",
"@com_connectrpc_connect//:connect",
"@com_github_google_uuid//:uuid",
@ -47,12 +49,14 @@ go_test(
"mocks_test.go",
"v1_test.go",
],
data = glob(["testdata/**"]),
embed = [":subscriptionsservice"],
deps = [
"//cmd/enterprise-portal/internal/database/subscriptions",
"//cmd/enterprise-portal/internal/database/utctime",
"//cmd/enterprise-portal/internal/samsm2m",
"//internal/license",
"//internal/slack",
"//lib/enterpriseportal/subscriptions/v1:subscriptions",
"//lib/errors",
"//lib/managedservicesplatform/iam",

View File

@ -15,6 +15,7 @@ import (
subscriptions "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions"
utctime "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime"
license "github.com/sourcegraph/sourcegraph/internal/license"
slack "github.com/sourcegraph/sourcegraph/internal/slack"
iam "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam"
)
@ -27,6 +28,9 @@ type MockStoreV1 struct {
// function object controlling the behavior of the method
// CreateEnterpriseSubscriptionLicenseKey.
CreateEnterpriseSubscriptionLicenseKeyFunc *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc
// EnvFunc is an instance of a mock function object controlling the
// behavior of the method Env.
EnvFunc *StoreV1EnvFunc
// GenerateSubscriptionIDFunc is an instance of a mock function object
// controlling the behavior of the method GenerateSubscriptionID.
GenerateSubscriptionIDFunc *StoreV1GenerateSubscriptionIDFunc
@ -64,6 +68,9 @@ type MockStoreV1 struct {
// NowFunc is an instance of a mock function object controlling the
// behavior of the method Now.
NowFunc *StoreV1NowFunc
// PostToSlackFunc is an instance of a mock function object controlling
// the behavior of the method PostToSlack.
PostToSlackFunc *StoreV1PostToSlackFunc
// RevokeEnterpriseSubscriptionLicenseFunc is an instance of a mock
// function object controlling the behavior of the method
// RevokeEnterpriseSubscriptionLicense.
@ -87,6 +94,11 @@ func NewMockStoreV1() *MockStoreV1 {
return
},
},
EnvFunc: &StoreV1EnvFunc{
defaultHook: func() (r0 string) {
return
},
},
GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{
defaultHook: func() (r0 string, r1 error) {
return
@ -142,6 +154,11 @@ func NewMockStoreV1() *MockStoreV1 {
return
},
},
PostToSlackFunc: &StoreV1PostToSlackFunc{
defaultHook: func(context.Context, *slack.Payload) (r0 error) {
return
},
},
RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{
defaultHook: func(context.Context, string, subscriptions.RevokeLicenseOpts) (r0 *subscriptions.LicenseWithConditions, r1 error) {
return
@ -169,6 +186,11 @@ func NewStrictMockStoreV1() *MockStoreV1 {
panic("unexpected invocation of MockStoreV1.CreateEnterpriseSubscriptionLicenseKey")
},
},
EnvFunc: &StoreV1EnvFunc{
defaultHook: func() string {
panic("unexpected invocation of MockStoreV1.Env")
},
},
GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{
defaultHook: func() (string, error) {
panic("unexpected invocation of MockStoreV1.GenerateSubscriptionID")
@ -224,6 +246,11 @@ func NewStrictMockStoreV1() *MockStoreV1 {
panic("unexpected invocation of MockStoreV1.Now")
},
},
PostToSlackFunc: &StoreV1PostToSlackFunc{
defaultHook: func(context.Context, *slack.Payload) error {
panic("unexpected invocation of MockStoreV1.PostToSlack")
},
},
RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{
defaultHook: func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) {
panic("unexpected invocation of MockStoreV1.RevokeEnterpriseSubscriptionLicense")
@ -249,6 +276,9 @@ func NewMockStoreV1From(i StoreV1) *MockStoreV1 {
CreateEnterpriseSubscriptionLicenseKeyFunc: &StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc{
defaultHook: i.CreateEnterpriseSubscriptionLicenseKey,
},
EnvFunc: &StoreV1EnvFunc{
defaultHook: i.Env,
},
GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{
defaultHook: i.GenerateSubscriptionID,
},
@ -282,6 +312,9 @@ func NewMockStoreV1From(i StoreV1) *MockStoreV1 {
NowFunc: &StoreV1NowFunc{
defaultHook: i.Now,
},
PostToSlackFunc: &StoreV1PostToSlackFunc{
defaultHook: i.PostToSlack,
},
RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{
defaultHook: i.RevokeEnterpriseSubscriptionLicense,
},
@ -413,6 +446,104 @@ func (c StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall) Results() []inter
return []interface{}{c.Result0, c.Result1}
}
// StoreV1EnvFunc describes the behavior when the Env method of the parent
// MockStoreV1 instance is invoked.
type StoreV1EnvFunc struct {
defaultHook func() string
hooks []func() string
history []StoreV1EnvFuncCall
mutex sync.Mutex
}
// Env delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockStoreV1) Env() string {
r0 := m.EnvFunc.nextHook()()
m.EnvFunc.appendCall(StoreV1EnvFuncCall{r0})
return r0
}
// SetDefaultHook sets function that is called when the Env method of the
// parent MockStoreV1 instance is invoked and the hook queue is empty.
func (f *StoreV1EnvFunc) SetDefaultHook(hook func() string) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// Env method of the parent MockStoreV1 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 *StoreV1EnvFunc) PushHook(hook func() string) {
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 *StoreV1EnvFunc) SetDefaultReturn(r0 string) {
f.SetDefaultHook(func() string {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *StoreV1EnvFunc) PushReturn(r0 string) {
f.PushHook(func() string {
return r0
})
}
func (f *StoreV1EnvFunc) nextHook() func() string {
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 *StoreV1EnvFunc) appendCall(r0 StoreV1EnvFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of StoreV1EnvFuncCall objects describing the
// invocations of this function.
func (f *StoreV1EnvFunc) History() []StoreV1EnvFuncCall {
f.mutex.Lock()
history := make([]StoreV1EnvFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// StoreV1EnvFuncCall is an object that describes an invocation of method
// Env on an instance of MockStoreV1.
type StoreV1EnvFuncCall struct {
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 string
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c StoreV1EnvFuncCall) Args() []interface{} {
return []interface{}{}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c StoreV1EnvFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// StoreV1GenerateSubscriptionIDFunc describes the behavior when the
// GenerateSubscriptionID method of the parent MockStoreV1 instance is
// invoked.
@ -1589,6 +1720,111 @@ func (c StoreV1NowFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// StoreV1PostToSlackFunc describes the behavior when the PostToSlack method
// of the parent MockStoreV1 instance is invoked.
type StoreV1PostToSlackFunc struct {
defaultHook func(context.Context, *slack.Payload) error
hooks []func(context.Context, *slack.Payload) error
history []StoreV1PostToSlackFuncCall
mutex sync.Mutex
}
// PostToSlack delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockStoreV1) PostToSlack(v0 context.Context, v1 *slack.Payload) error {
r0 := m.PostToSlackFunc.nextHook()(v0, v1)
m.PostToSlackFunc.appendCall(StoreV1PostToSlackFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the PostToSlack method
// of the parent MockStoreV1 instance is invoked and the hook queue is
// empty.
func (f *StoreV1PostToSlackFunc) SetDefaultHook(hook func(context.Context, *slack.Payload) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// PostToSlack method of the parent MockStoreV1 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 *StoreV1PostToSlackFunc) PushHook(hook func(context.Context, *slack.Payload) 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 *StoreV1PostToSlackFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, *slack.Payload) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *StoreV1PostToSlackFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, *slack.Payload) error {
return r0
})
}
func (f *StoreV1PostToSlackFunc) nextHook() func(context.Context, *slack.Payload) 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 *StoreV1PostToSlackFunc) appendCall(r0 StoreV1PostToSlackFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of StoreV1PostToSlackFuncCall objects
// describing the invocations of this function.
func (f *StoreV1PostToSlackFunc) History() []StoreV1PostToSlackFuncCall {
f.mutex.Lock()
history := make([]StoreV1PostToSlackFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// StoreV1PostToSlackFuncCall is an object that describes an invocation of
// method PostToSlack on an instance of MockStoreV1.
type StoreV1PostToSlackFuncCall 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 *slack.Payload
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c StoreV1PostToSlackFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c StoreV1PostToSlackFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// StoreV1RevokeEnterpriseSubscriptionLicenseFunc describes the behavior
// when the RevokeEnterpriseSubscriptionLicense method of the parent
// MockStoreV1 instance is invoked.

View File

@ -0,0 +1,8 @@
A new license was created for subscription <https://sourcegraph.com/site-admin/dotcom/product/subscriptions/es_sub-id?env=dev|display-name>:
• *Expiration (UTC)*: Jul 9, 2024 10:39pm UTC (1.2 days remaining)
• *Expiration (PT)*: Jul 9, 2024 3:39pm PDT
• *User count*: 123
• *License tags*: `foo`
• *Salesforce subscription*: salesforce-subscription-id
• *Salesforce opportunity*: <https://sourcegraph2020.lightning.force.com/lightning/r/Opportunity/salesforce-opp-id/view|salesforce-opp-id>

View File

@ -5,7 +5,9 @@ import (
"database/sql"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"connectrpc.com/connect"
"github.com/sourcegraph/log"
@ -27,6 +29,7 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/dotcomdb"
"github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/samsm2m"
"github.com/sourcegraph/sourcegraph/internal/collections"
"github.com/sourcegraph/sourcegraph/internal/slack"
"github.com/sourcegraph/sourcegraph/internal/trace"
)
@ -742,6 +745,18 @@ func (s *handlerV1) CreateEnterpriseSubscriptionLicense(ctx context.Context, req
return nil, connectutil.InternalError(ctx, logger, err, "failed to create license key")
}
if err := s.store.PostToSlack(
context.WithoutCancel(ctx),
&slack.Payload{
Text: renderLicenseKeyCreationSlackMessage(
s.store.Now(),
s.store.Env(),
sub.Subscription,
licenseKey),
},
); err != nil {
logger.Info("failed to post license creation to Slack", log.Error(err))
}
default:
return nil, connect.NewError(connect.CodeInvalidArgument, errors.Newf("unsupported licnese type %T", data))
}
@ -961,3 +976,43 @@ func (s *handlerV1) UpdateEnterpriseSubscriptionMembership(ctx context.Context,
}
return connect.NewResponse(&subscriptionsv1.UpdateEnterpriseSubscriptionMembershipResponse{}), nil
}
const slackLicenseKeyCreationMessageFmt = `
A new license was created for subscription <https://sourcegraph.com/site-admin/dotcom/product/subscriptions/%[1]s?env=%[2]s|%[3]s>:
*Expiration (UTC)*: %[4]s (%[5]s days remaining)
*Expiration (PT)*: %[6]s
*User count*: %[7]s
*License tags*: %[8]s
*Salesforce subscription*: %[9]s
*Salesforce opportunity*: <https://sourcegraph2020.lightning.force.com/lightning/r/Opportunity/%[10]s/view|%[10]s>
`
func renderLicenseKeyCreationSlackMessage(
now utctime.Time,
env string,
sub subscriptions.Subscription,
key *subscriptions.DataLicenseKey,
) string {
pacificLoc, _ := time.LoadLocation("America/Los_Angeles")
// Prefix internal ID for external usage
externalSubID := subscriptionsv1.EnterpriseSubscriptionIDPrefix + sub.ID
// Safely dereference optional properties
sfSubscriptionID := pointers.Deref(sub.SalesforceSubscriptionID, "unknown")
sfOpportunityID := pointers.Deref(key.Info.SalesforceOpportunityID, "unknown")
return strings.TrimSpace(fmt.Sprintf(slackLicenseKeyCreationMessageFmt,
externalSubID,
env,
pointers.Deref(sub.DisplayName, externalSubID),
key.Info.ExpiresAt.UTC().Format("Jan 2, 2006 3:04pm MST"),
strconv.FormatFloat(key.Info.ExpiresAt.UTC().Sub(now.AsTime()).Hours()/24, 'f', 1, 64),
key.Info.ExpiresAt.In(pacificLoc).Format("Jan 2, 2006 3:04pm MST"),
strconv.FormatUint(uint64(key.Info.UserCount), 10),
"`"+strings.Join(key.Info.Tags, "`, `")+"`",
sfSubscriptionID,
sfOpportunityID,
))
}

View File

@ -7,6 +7,8 @@ import (
"github.com/google/uuid"
"golang.org/x/crypto/ssh"
"github.com/sourcegraph/log"
sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
clientsv1 "github.com/sourcegraph/sourcegraph-accounts-sdk-go/clients/v1"
@ -14,9 +16,11 @@ import (
"github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions"
"github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime"
"github.com/sourcegraph/sourcegraph/internal/license"
"github.com/sourcegraph/sourcegraph/internal/slack"
subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1"
"github.com/sourcegraph/sourcegraph/lib/errors"
"github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam"
"github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/runtime/contract"
)
// StoreV1 is the data layer carrier for subscriptions service v1. This interface
@ -26,6 +30,8 @@ type StoreV1 interface {
// Now provides the current time. It should always be used instead of
// utctime.Now() or time.Now() for ease of mocking in tests.
Now() utctime.Time
// Env provides the current Enterprise Portal environment.
Env() string
// GenerateSubscriptionID generates a new subscription ID for subscription
// creation.
@ -73,6 +79,10 @@ type StoreV1 interface {
// IAMCheck checks whether a relationship exists (thus permission allowed) using
// the given tuple key as the check condition.
IAMCheck(ctx context.Context, opts iam.CheckOptions) (allowed bool, _ error)
// PostToSlack sends a Slack message to the destination configured for
// subscription API events, such as license creation.
PostToSlack(ctx context.Context, payload *slack.Payload) error
}
// licenseKeysStore groups mechanisms specific to the license type
@ -91,39 +101,53 @@ type licenseKeysStore interface {
}
type storeV1 struct {
logger log.Logger
env string
db *database.DB
SAMSClient *sams.ClientV1
IAMClient *iam.ClientV1
// LicenseKeySigner may be nil if not configured for key signing.
LicenseKeySigner ssh.Signer
LicenseKeyRequiredTags []string
SlackWebhookURL *string
}
type NewStoreV1Options struct {
Contract contract.Contract
DB *database.DB
SAMSClient *sams.ClientV1
IAMClient *iam.ClientV1
LicenseKeySigner ssh.Signer
LicenseKeyRequiredTags []string
SlackWebhookURL *string
}
var errStoreUnimplemented = errors.New("unimplemented")
// NewStoreV1 returns a new StoreV1 using the given resource handles.
func NewStoreV1(opts NewStoreV1Options) StoreV1 {
func NewStoreV1(logger log.Logger, opts NewStoreV1Options) StoreV1 {
return &storeV1{
logger: logger.Scoped("subscriptions.v1.store"),
env: opts.Contract.EnvironmentID,
db: opts.DB,
SAMSClient: opts.SAMSClient,
IAMClient: opts.IAMClient,
LicenseKeySigner: opts.LicenseKeySigner,
LicenseKeyRequiredTags: opts.LicenseKeyRequiredTags,
SlackWebhookURL: opts.SlackWebhookURL,
}
}
func (s *storeV1) Now() utctime.Time { return utctime.Now() }
func (s *storeV1) Env() string { return s.env }
func (s *storeV1) GenerateSubscriptionID() (string, error) {
id, err := uuid.NewRandom()
if err != nil {
@ -212,3 +236,12 @@ func (s *storeV1) IAMWrite(ctx context.Context, opts iam.WriteOptions) error {
func (s *storeV1) IAMCheck(ctx context.Context, opts iam.CheckOptions) (allowed bool, _ error) {
return s.IAMClient.Check(ctx, opts)
}
func (s *storeV1) PostToSlack(ctx context.Context, payload *slack.Payload) error {
if s.SlackWebhookURL == nil {
s.logger.Info("PostToSlack",
log.String("text", payload.Text))
return nil
}
return slack.New(*s.SlackWebhookURL).Post(ctx, payload)
}

View File

@ -925,6 +925,8 @@ func TestHandlerV1_CreateEnterpriseSubscriptionLicense(t *testing.T) {
}
if tc.wantKeyOpts != nil {
mockrequire.CalledOnce(t, h.mockStore.CreateEnterpriseSubscriptionLicenseKeyFunc)
// Successful creation should get a Slack message as well
mockrequire.CalledOnce(t, h.mockStore.PostToSlackFunc)
} else {
mockrequire.NotCalled(t, h.mockStore.CreateEnterpriseSubscriptionLicenseKeyFunc)
}
@ -1210,3 +1212,26 @@ func TestHandlerV1_UpdateEnterpriseSubscriptionMembership(t *testing.T) {
})
}
}
func TestRenderLicenseKeyCreationSlackMessage(t *testing.T) {
mockTime := utctime.FromTime(time.Date(2024, 7, 8, 16, 39, 16, 0, time.UTC))
text := renderLicenseKeyCreationSlackMessage(
mockTime,
"dev",
subscriptions.Subscription{
ID: "sub-id",
DisplayName: pointers.Ptr("display-name"),
SalesforceSubscriptionID: pointers.Ptr("salesforce-subscription-id"),
},
&subscriptions.DataLicenseKey{
Info: license.Info{
UserCount: 123,
Tags: []string{"foo"},
SalesforceOpportunityID: pointers.Ptr("salesforce-opp-id"),
ExpiresAt: mockTime.AsTime().Add(30 * time.Hour),
},
},
)
autogold.ExpectFile(t, autogold.Raw(text))
}

View File

@ -44,6 +44,8 @@ type Config struct {
}
LicenseExpirationChecker licenseexpiration.Config
SubscriptionsServiceSlackWebhookURL *string
}
type SAMSConfig struct {
@ -118,6 +120,10 @@ func (c *Config) Load(env *runtime.Env) {
"Interval at which to run license expiration checks. If not set, checks are not run.")
c.LicenseExpirationChecker.SlackWebhookURL = env.GetOptional(
"LICENSE_EXPIRATION_CHECKER_SLACK_WEBHOOK_URL",
"Destination webhook for expired licenses. If not set, messages are logged.",
"Destination webhook for expiring licenses. If not set, messages are logged.",
)
c.SubscriptionsServiceSlackWebhookURL = env.GetOptional(
"SUBSCRIPTIONS_SERVICE_SLACK_WEBHOOK_URL",
"Destination webhook for subscription API events, such as license creation. If not set, messages are logged.")
}

View File

@ -119,12 +119,15 @@ func (Service) Initialize(ctx context.Context, logger log.Logger, contract runti
logger,
httpServer,
subscriptionsservice.NewStoreV1(
logger,
subscriptionsservice.NewStoreV1Options{
Contract: contract.Contract,
DB: dbHandle,
SAMSClient: samsClient,
IAMClient: iamClient,
LicenseKeySigner: config.LicenseKeys.Signer,
LicenseKeyRequiredTags: config.LicenseKeys.RequiredTags,
SlackWebhookURL: config.SubscriptionsServiceSlackWebhookURL,
},
),
connect.WithInterceptors(otelConnctInterceptor),

View File

@ -475,6 +475,7 @@ commands:
DIAGNOSTICS_SECRET: sekret
SRC_LOG_LEVEL: debug
GRPC_WEB_UI_ENABLED: 'true'
ENVIRONMENT_ID: local
# Connects to local database, so include all licenses from local DB
DOTCOM_INCLUDE_PRODUCTION_LICENSES: 'true'
# Used for authentication