diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel index d9d22945ab0..30ea1ccd4df 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel +++ b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel @@ -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", diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go b/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go index b3c864cc327..03589e873cf 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go @@ -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. diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/testdata/TestRenderLicenseKeyCreationSlackMessage.golden b/cmd/enterprise-portal/internal/subscriptionsservice/testdata/TestRenderLicenseKeyCreationSlackMessage.golden new file mode 100644 index 00000000000..e33c2ad4458 --- /dev/null +++ b/cmd/enterprise-portal/internal/subscriptionsservice/testdata/TestRenderLicenseKeyCreationSlackMessage.golden @@ -0,0 +1,8 @@ +A new license was created for subscription : + +• *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*: \ No newline at end of file diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go index ee7e5e9a14c..d88575b3417 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go @@ -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 : + +• *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*: +` + +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, + )) +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go index b58d9de8e13..dd4ad0ec385 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go @@ -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) +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go index 9853d74880f..c6d3ceaea6a 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go @@ -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)) +} diff --git a/cmd/enterprise-portal/service/config.go b/cmd/enterprise-portal/service/config.go index daf25ad899f..a67fa08c16e 100644 --- a/cmd/enterprise-portal/service/config.go +++ b/cmd/enterprise-portal/service/config.go @@ -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.") } diff --git a/cmd/enterprise-portal/service/service.go b/cmd/enterprise-portal/service/service.go index 57891682950..26dbce32b3e 100644 --- a/cmd/enterprise-portal/service/service.go +++ b/cmd/enterprise-portal/service/service.go @@ -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), diff --git a/sg.config.yaml b/sg.config.yaml index bbca3df8649..d31bcd7f720 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -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