Skip to content

Commit 4654866

Browse files
authored
feat: Support Contract Listing For AWS Marketplace (#1889)
Implements https://catalog.workshops.aws/mpseller/en-US/container/integrate-contract
1 parent a881fac commit 4654866

File tree

8 files changed

+259
-22
lines changed

8 files changed

+259
-22
lines changed

examples/simple_plugin/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
2222
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
2323
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
24+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4 // indirect
2425
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4 // indirect
2526
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
2627
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect

examples/simple_plugin/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbL
2525
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
2626
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
2727
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
28+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4 h1:8tRjT7S8LxBRNRP3KtdV9vj9dJPzG1yDvRIqVmznZII=
29+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4/go.mod h1:AhruhNzkEGM6NxQzGhc0gWvaj/o8FZi/cCoGymOVxyo=
2830
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4 h1:I9yxA99P3rbkzhv8iDykQcel7n03PmlK8GO6NDpOkj0=
2931
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4/go.mod h1:YAiuhtKyLLPdouuDXeFWh4nrDrMqwQqukNvDSyhltbU=
3032
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/apache/arrow/go/v17 v17.0.0
77
github.com/aws/aws-sdk-go-v2 v1.30.4
88
github.com/aws/aws-sdk-go-v2/config v1.27.31
9+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4
910
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4
1011
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
1112
github.com/cloudquery/cloudquery-api-go v1.13.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbL
2525
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
2626
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
2727
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
28+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4 h1:8tRjT7S8LxBRNRP3KtdV9vj9dJPzG1yDvRIqVmznZII=
29+
github.com/aws/aws-sdk-go-v2/service/licensemanager v1.27.4/go.mod h1:AhruhNzkEGM6NxQzGhc0gWvaj/o8FZi/cCoGymOVxyo=
2830
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4 h1:I9yxA99P3rbkzhv8iDykQcel7n03PmlK8GO6NDpOkj0=
2931
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.23.4/go.mod h1:YAiuhtKyLLPdouuDXeFWh4nrDrMqwQqukNvDSyhltbU=
3032
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 h1:zCsFCKvbj25i7p1u94imVoO447I/sFv8qq+lGJhRN0c=

premium/mocks/licensemanager.go

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

premium/offline.go

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package premium
22

33
import (
4+
"context"
45
"crypto/ed25519"
56
_ "embed"
67
"encoding/hex"
78
"encoding/json"
89
"errors"
10+
"fmt"
911
"os"
1012
"path/filepath"
1113
"slices"
1214
"strings"
1315
"time"
1416

17+
"github.com/aws/aws-sdk-go-v2/aws"
18+
awsConfig "github.com/aws/aws-sdk-go-v2/config"
19+
"github.com/aws/aws-sdk-go-v2/service/licensemanager"
20+
"github.com/aws/aws-sdk-go-v2/service/licensemanager/types"
1521
"github.com/cloudquery/plugin-sdk/v4/plugin"
22+
"github.com/google/uuid"
1623
"github.com/rs/zerolog"
1724
)
1825

@@ -41,20 +48,88 @@ var publicKey string
4148

4249
var timeFunc = time.Now
4350

44-
func ValidateLicense(logger zerolog.Logger, meta plugin.Meta, licenseFileOrDirectory string) error {
45-
fi, err := os.Stat(licenseFileOrDirectory)
51+
const awsProductSKU = "prod-2trmtbe74klkg"
52+
53+
//go:generate mockgen -package=mocks -destination=../premium/mocks/licensemanager.go -source=offline.go AWSLicenseManagerInterface
54+
type AWSLicenseManagerInterface interface {
55+
CheckoutLicense(ctx context.Context, params *licensemanager.CheckoutLicenseInput, optFns ...func(*licensemanager.Options)) (*licensemanager.CheckoutLicenseOutput, error)
56+
}
57+
58+
type CQLicenseClient struct {
59+
logger zerolog.Logger
60+
meta plugin.Meta
61+
licenseFileOrDirectory string
62+
awsLicenseManagerClient AWSLicenseManagerInterface
63+
isMarketplaceLicense bool
64+
}
65+
66+
type LicenseClientOptions func(updater *CQLicenseClient)
67+
68+
func WithMeta(meta plugin.Meta) LicenseClientOptions {
69+
return func(cl *CQLicenseClient) {
70+
cl.meta = meta
71+
}
72+
}
73+
74+
func WithLicenseFileOrDirectory(licenseFileOrDirectory string) LicenseClientOptions {
75+
return func(cl *CQLicenseClient) {
76+
cl.licenseFileOrDirectory = licenseFileOrDirectory
77+
}
78+
}
79+
80+
func WithAWSLicenseManagerClient(awsLicenseManagerClient AWSLicenseManagerInterface) LicenseClientOptions {
81+
return func(cl *CQLicenseClient) {
82+
cl.awsLicenseManagerClient = awsLicenseManagerClient
83+
}
84+
}
85+
86+
func NewLicenseClient(ctx context.Context, logger zerolog.Logger, ops ...LicenseClientOptions) (CQLicenseClient, error) {
87+
cl := CQLicenseClient{
88+
logger: logger,
89+
isMarketplaceLicense: os.Getenv("CQ_AWS_MARKETPLACE_LICENSE") == "true",
90+
}
91+
92+
for _, op := range ops {
93+
op(&cl)
94+
}
95+
96+
if cl.isMarketplaceLicense && cl.awsLicenseManagerClient == nil {
97+
cfg, err := awsConfig.LoadDefaultConfig(ctx)
98+
if err != nil {
99+
return cl, fmt.Errorf("failed to load AWS config: %w", err)
100+
}
101+
cl.awsLicenseManagerClient = licensemanager.NewFromConfig(cfg)
102+
}
103+
104+
return cl, nil
105+
}
106+
107+
func (lc CQLicenseClient) ValidateLicense(ctx context.Context) error {
108+
// License can be provided via environment variable for AWS Marketplace or CLI flag
109+
switch {
110+
case lc.isMarketplaceLicense:
111+
return lc.validateMarketplaceLicense(ctx)
112+
case lc.licenseFileOrDirectory != "":
113+
return lc.validateCQLicense()
114+
default:
115+
return ErrLicenseNotApplicable
116+
}
117+
}
118+
119+
func (lc CQLicenseClient) validateCQLicense() error {
120+
fi, err := os.Stat(lc.licenseFileOrDirectory)
46121
if err != nil {
47122
return err
48123
}
49124
if !fi.IsDir() {
50-
return validateLicenseFile(logger, meta, licenseFileOrDirectory)
125+
return lc.validateLicenseFile(lc.licenseFileOrDirectory)
51126
}
52127

53128
found := false
54129
var lastError error
55-
err = filepath.WalkDir(licenseFileOrDirectory, func(path string, d os.DirEntry, err error) error {
130+
err = filepath.WalkDir(lc.licenseFileOrDirectory, func(path string, d os.DirEntry, err error) error {
56131
if d.IsDir() {
57-
if path == licenseFileOrDirectory {
132+
if path == lc.licenseFileOrDirectory {
58133
return nil
59134
}
60135
return filepath.SkipDir
@@ -67,8 +142,8 @@ func ValidateLicense(logger zerolog.Logger, meta plugin.Meta, licenseFileOrDirec
67142
return nil
68143
}
69144

70-
logger.Debug().Str("path", path).Msg("considering license file")
71-
lastError = validateLicenseFile(logger, meta, path)
145+
lc.logger.Debug().Str("path", path).Msg("considering license file")
146+
lastError = lc.validateLicenseFile(path)
72147
switch lastError {
73148
case nil:
74149
found = true
@@ -91,7 +166,7 @@ func ValidateLicense(logger zerolog.Logger, meta plugin.Meta, licenseFileOrDirec
91166
return errors.New("failed to validate license directory")
92167
}
93168

94-
func validateLicenseFile(logger zerolog.Logger, meta plugin.Meta, licenseFile string) error {
169+
func (lc CQLicenseClient) validateLicenseFile(licenseFile string) error {
95170
licenseContents, err := os.ReadFile(licenseFile)
96171
if err != nil {
97172
return err
@@ -103,14 +178,14 @@ func validateLicenseFile(logger zerolog.Logger, meta plugin.Meta, licenseFile st
103178
}
104179

105180
if len(l.Plugins) > 0 {
106-
ref := strings.Join([]string{meta.Team, string(meta.Kind), meta.Name}, "/")
107-
teamRef := meta.Team + "/*"
181+
ref := strings.Join([]string{lc.meta.Team, string(lc.meta.Kind), lc.meta.Name}, "/")
182+
teamRef := lc.meta.Team + "/*"
108183
if !slices.Contains(l.Plugins, ref) && !slices.Contains(l.Plugins, teamRef) {
109184
return ErrLicenseNotApplicable
110185
}
111186
}
112187

113-
return l.IsValid(logger)
188+
return l.IsValid(lc.logger)
114189
}
115190

116191
func UnpackLicense(lic []byte) (*License, error) {
@@ -158,3 +233,28 @@ func (l *License) IsValid(logger zerolog.Logger) error {
158233
msg.Time("expires_at", l.ExpiresAt).Msgf("Offline license for %s loaded.", l.LicensedTo)
159234
return nil
160235
}
236+
237+
func (lc CQLicenseClient) validateMarketplaceLicense(ctx context.Context) error {
238+
clientToken := uuid.New()
239+
240+
resp, err := lc.awsLicenseManagerClient.CheckoutLicense(ctx, &licensemanager.CheckoutLicenseInput{
241+
CheckoutType: types.CheckoutTypeProvisional,
242+
ClientToken: aws.String(clientToken.String()),
243+
ProductSKU: aws.String(awsProductSKU),
244+
Entitlements: []types.EntitlementData{
245+
{
246+
Name: aws.String("Unlimited"),
247+
Unit: types.EntitlementDataUnitNone,
248+
},
249+
},
250+
// This is hardcoded for AWS Marketplace, because this is the only supported value for marketplace licenses
251+
KeyFingerprint: aws.String("aws:294406891311:AWS/Marketplace:issuer-fingerprint"),
252+
})
253+
if err != nil {
254+
return fmt.Errorf("failed to checkout license: %w", err)
255+
}
256+
if len(resp.EntitlementsAllowed) == 0 {
257+
return errors.New("no entitlements provisioned")
258+
}
259+
return nil
260+
}

premium/offline_test.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package premium
22

33
import (
4+
"context"
5+
"fmt"
46
"os"
57
"path/filepath"
68
"testing"
79
"time"
810

11+
"github.com/aws/aws-sdk-go-v2/aws"
12+
"github.com/aws/aws-sdk-go-v2/service/licensemanager"
13+
"github.com/aws/aws-sdk-go-v2/service/licensemanager/types"
14+
"github.com/cloudquery/plugin-sdk/v4/faker"
915
"github.com/cloudquery/plugin-sdk/v4/plugin"
16+
"github.com/cloudquery/plugin-sdk/v4/premium/mocks"
17+
"github.com/golang/mock/gomock"
1018
"github.com/rs/zerolog"
19+
"github.com/stretchr/testify/assert"
1120
"github.com/stretchr/testify/require"
1221
)
1322

@@ -153,12 +162,76 @@ func licenseTest(inputPath string, meta plugin.Meta, timeIs time.Time, expectErr
153162
timeFunc = func() time.Time {
154163
return timeIs
155164
}
156-
157-
err := ValidateLicense(zerolog.Nop(), meta, inputPath)
165+
licenseClient, err := NewLicenseClient(context.TODO(), zerolog.Nop(), WithMeta(meta), WithLicenseFileOrDirectory(inputPath))
166+
require.NoError(t, err)
167+
err = licenseClient.ValidateLicense(context.TODO())
158168
if expectError == nil {
159169
require.NoError(t, err)
160170
} else {
161171
require.ErrorIs(t, err, expectError)
162172
}
163173
}
164174
}
175+
176+
func TestValidateMarketplaceLicense(t *testing.T) {
177+
ctrl := gomock.NewController(t)
178+
m := mocks.NewMockAWSLicenseManagerInterface(ctrl)
179+
out := licensemanager.CheckoutLicenseOutput{}
180+
in := licenseInput{
181+
CheckoutLicenseInput: licensemanager.CheckoutLicenseInput{
182+
CheckoutType: types.CheckoutTypeProvisional,
183+
ProductSKU: aws.String(awsProductSKU),
184+
Entitlements: []types.EntitlementData{
185+
{
186+
Name: aws.String("Unlimited"),
187+
Unit: types.EntitlementDataUnitNone,
188+
},
189+
},
190+
KeyFingerprint: aws.String("aws:294406891311:AWS/Marketplace:issuer-fingerprint"),
191+
},
192+
}
193+
194+
assert.NoError(t, faker.FakeObject(&out))
195+
m.EXPECT().CheckoutLicense(gomock.Any(), in).Return(&out, nil)
196+
t.Setenv("CQ_AWS_MARKETPLACE_LICENSE", "true")
197+
198+
licenseClient, err := NewLicenseClient(context.TODO(), zerolog.Nop(), WithAWSLicenseManagerClient(m))
199+
require.NoError(t, err)
200+
require.NoError(t, licenseClient.ValidateLicense(context.TODO()))
201+
}
202+
203+
type licenseInput struct {
204+
licensemanager.CheckoutLicenseInput
205+
}
206+
207+
func (li licenseInput) Matches(x any) bool {
208+
testInput, ok := x.(*licensemanager.CheckoutLicenseInput)
209+
if !ok {
210+
return false
211+
}
212+
213+
if testInput.CheckoutType != li.CheckoutType {
214+
return false
215+
}
216+
217+
for i, ent := range testInput.Entitlements {
218+
if aws.ToString(ent.Name) != aws.ToString(li.Entitlements[i].Name) {
219+
return false
220+
}
221+
if aws.ToString(ent.Value) != aws.ToString(li.Entitlements[i].Value) {
222+
return false
223+
}
224+
}
225+
226+
if aws.ToString(testInput.KeyFingerprint) != aws.ToString(li.KeyFingerprint) {
227+
return false
228+
}
229+
if aws.ToString(testInput.ProductSKU) != aws.ToString(li.ProductSKU) {
230+
return false
231+
}
232+
return true
233+
}
234+
235+
func (li licenseInput) String() string {
236+
return fmt.Sprintf("{CheckoutType:%s Entitlements:%v KeyFingerprint:%s ProductSKU:%s}", li.CheckoutType, li.Entitlements, *li.KeyFingerprint, *li.ProductSKU)
237+
}

0 commit comments

Comments
 (0)