mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 11:01:44 +00:00
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:
parent
2649987bac
commit
043e780590
@ -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",
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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.")
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user