From 304b097f2d58ed37d50551662cb7f0b0f79831f3 Mon Sep 17 00:00:00 2001 From: Oscar Carrasquero Date: Tue, 10 Sep 2024 10:52:48 +0200 Subject: [PATCH 1/4] Product's fulfillment URL is updated via Catalog API. Templates requires onw ProductId instead of the ProductCode. --- template.yaml | 170 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 166 insertions(+), 4 deletions(-) diff --git a/template.yaml b/template.yaml index 72dfcdb..53c5a40 100644 --- a/template.yaml +++ b/template.yaml @@ -50,7 +50,8 @@ Parameters: Description: "This is the AWS region of the SNS Entitlement and Subscription topics for your product." AllowedValues: - us-east-1 - ProductCode: + + ProductId: Type: String AllowedPattern: ".*" @@ -290,7 +291,7 @@ Resources: MySQSEvent: Type: SNS Properties: - Topic: !Sub 'arn:aws:sns:${SNSRegion}:${SNSAccountID}:aws-mp-entitlement-notification-${ProductCode}' + Topic: !Sub 'arn:aws:sns:${SNSRegion}:${SNSAccountID}:aws-mp-entitlement-notification-${GetProductCode.ProductCode}' Region: !Sub '${SNSRegion}' SqsSubscription: BatchSize: 1 @@ -321,7 +322,7 @@ Resources: Type: SNS Properties: #Topic: !Ref SubscriptionSNSTopic - Topic: !Sub 'arn:aws:sns:${SNSRegion}:${SNSAccountID}:aws-mp-subscription-notification-${ProductCode}' + Topic: !Sub 'arn:aws:sns:${SNSRegion}:${SNSAccountID}:aws-mp-subscription-notification-${GetProductCode.ProductCode}' Region: !Sub '${SNSRegion}' SqsSubscription: true @@ -425,7 +426,7 @@ Resources: Runtime: nodejs18.x Environment: Variables: - ProductCode: !Ref ProductCode + ProductCode: !GetAtt GetProductCode.ProductCode AWSMarketplaceMeteringRecordsTableName: !Ref AWSMarketplaceMeteringRecordsTableName Policies: - DynamoDBWritePolicy: @@ -1011,7 +1012,167 @@ Resources: - !Ref AWS::StackId RetentionInDays: 7 + GetProductCode: + Type: Custom::Lambda + Properties: + ServiceToken: !GetAtt GetProductCodeCustomResource.Arn + ProductId: !Ref ProductId + + GetProductCodeCustomResource: + Type: AWS::Lambda::Function + Properties: + Role: !GetAtt CAPILambdasExecutionRole.Arn + Runtime: nodejs18.x + Handler: index.handler + Code: + ZipFile: | + const { MarketplaceCatalogClient, DescribeEntityCommand } = require("@aws-sdk/client-marketplace-catalog"); + const response = require('cfn-response'); + exports.handler = async (event, context) => { + context.callbackWaitsForEmptyEventLoop = true; + console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)); + const client = new MarketplaceCatalogClient({ region: 'us-east-1' }); + const productId = event.ResourceProperties.ProductId; // Assuming the product ID is passed as an event parameter + + try { + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + // Fetch the product details from AWS Marketplace + const command = new DescribeEntityCommand({ + Catalog: 'AWSMarketplace', + EntityId: productId, + EntityType: 'Product' + }); + const resp = await client.send(command); + + // Extract the product code from the response + const productCode = resp.DetailsDocument.Description.ProductCode; + + const responseData = { + ProductCode: productCode + }; + + await response.send(event, context, 'SUCCESS', responseData); + } else if (event.RequestType === 'Delete') { + // No action needed for delete + await response.send(event, context, 'SUCCESS', {}); + } else { + await response.send(event, context, 'FAILED', { error: 'Invalid request type' }); + } + } catch (error) { + console.error('Error:', error); + await response.send(event, context, 'FAILED', { error: 'Failed to fetch product code' }); + } + }; + + CAPILambdasExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - 'sts:AssumeRole' + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + - arn:aws:iam::aws:policy/AWSMarketplaceSellerFullAccess + + UpdateFulfillmentURL: + Type: Custom::Lambda + Properties: + ServiceToken: !GetAtt UpdateFulfillmentURLCustomResource.Arn + ProductId: !Ref ProductId + # FulfillmentUrl: 'https://cachicamo.org' + FulfillmentUrl: !If [ + CreateWeb, + !Sub "https://${CloudfrontDistribution.DomainName}/redirectmarketplacetoken", + !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/redirectmarketplacetoken" + ] + + UpdateFulfillmentURLCustomResource: + Type: AWS::Lambda::Function + Properties: + Role: !GetAtt CAPILambdasExecutionRole.Arn + Runtime: nodejs18.x + Handler: index.handler + Code: + ZipFile: | + const { MarketplaceCatalogClient, DescribeEntityCommand, StartChangeSetCommand } = require("@aws-sdk/client-marketplace-catalog"); + const response = require('cfn-response'); + exports.handler = async (event, context) => { + console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)); + const client = new MarketplaceCatalogClient({ region: 'us-east-1' }); + const productId = event.ResourceProperties.ProductId; + const fulfillmentUrl = event.ResourceProperties.FulfillmentUrl; + try { + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + // Fetch the fulfillment url id to be able to update the fulfillment url + let command = new DescribeEntityCommand({ + Catalog: 'AWSMarketplace', + EntityId: productId, + EntityType: 'Product' + }); + let resp = await client.send(command); + console.debug("DescribeEntityCommand:\n" + JSON.stringify(resp)); + + + const fulfillmentUrlID = resp.DetailsDocument.Versions[0].DeliveryOptions[0].Id + console.debug("FullfilmentId:\n" + fulfillmentUrlID); + + const details = { + DeliveryOptions : [{ + Id: fulfillmentUrlID, + Details: { + SaaSUrlDeliveryOptionDetails: { + FulfillmentUrl: fulfillmentUrl + } + } + }] + }; + console.debug("details:\n" + JSON.stringify(details)); + + const startChangeSetInput = { + Catalog: 'AWSMarketplace', + ChangeSet: [ + { + ChangeType: 'UpdateDeliveryOptions', + Entity: { + Identifier: productId, + Type: 'SaaSProduct@1.0' + }, + Details: JSON.stringify(details) + } + ] + }; + console.debug("startChangeSetInput:\n" + JSON.stringify(startChangeSetInput)); + + command = new StartChangeSetCommand(startChangeSetInput); + resp = await client.send(command); + console.debug("StartChangeSetResp: \n" + JSON.stringify(resp)); + + const responseData = { + StartChangeSetResp: JSON.stringify(resp) + }; + + await response.send(event, context, 'SUCCESS', responseData); + } else if (event.RequestType === 'Delete') { + // No action needed for delete + await response.send(event, context, 'SUCCESS', {}); + } else { + await response.send(event, context, 'FAILED', { error: 'Invalid request type' }); + } + } catch (error) { + console.error('Error:', error); + await response.send(event, context, 'FAILED', { error: 'Failed to update fulfillment url' }); + } + }; + + Outputs: + CrossAccountRole: Description: This is the cross account role ARN. Value: @@ -1037,6 +1198,7 @@ Outputs: !Sub "https://${CloudfrontDistribution.DomainName}/index.html", "N/A" ] + MarketplaceFulfillmentURL: Description: This is the Marketplace fulfillment URL. Value: From 75cf928ccea0d5653abcc73e7a19075c43b5d690 Mon Sep 17 00:00:00 2001 From: Oscar Carrasquero Date: Tue, 10 Sep 2024 10:56:51 +0200 Subject: [PATCH 2/4] Updated Readme with ProductId --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19e0d6c..134b36d 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ To build and deploy your application for the first time, complete the following NewSubscribersTableName | Name for the New Subscribers Table; Default value: AWSMarketplaceSubscribers AWSMarketplaceMeteringRecordsTableName | Name for the Metering Records Table; Default value: AWSMarketplaceMeteringRecords TypeOfSaaSListing | allowed values: contracts_with_subscription, contracts, subscriptions; Default value: contracts_with_subscription - ProductCode | Product code provided from AWS Marketplace + ProductId | Product id provided from AWS Marketplace MarketplaceTechAdminEmail | Email to be notified on changes requiring action MarketplaceSellerEmail | (Optional) Seller email address, verified in SES and in 'Production' mode. See [Verify an email address](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses-procedure.html) for instruction to verify email addresses. SNSAccountID | AWS account ID hosting the Entitlements and Subscriptions SNS topics. Leave as default. From c31ca403783ea5e360bb677d28fc2e35ee974851 Mon Sep 17 00:00:00 2001 From: Oscar Carrasquero Date: Wed, 11 Sep 2024 15:42:17 +0200 Subject: [PATCH 3/4] Parameter was added to control fulfilment page url update. Scoped down Customer Resource's Policy --- template.yaml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/template.yaml b/template.yaml index 53c5a40..57636a1 100644 --- a/template.yaml +++ b/template.yaml @@ -88,6 +88,14 @@ Parameters: - "true" - "false" + UpdateFulfillmentURL: + Default: "false" + Type: String + Description: "WARNING: This will update your product's fulfillment URL automatically. Be careful if your product is already public" + AllowedValues: + - "true" + - "false" + Conditions: CreateEntitlementLogic: Fn::Or: @@ -102,7 +110,7 @@ Conditions: CreateWeb: !Equals [!Ref CreateRegistrationWebPage, true] Buyernotificationemail: !Not [!Equals [!Ref MarketplaceSellerEmail, ""]] CreateCrossAccount: !Equals [!Ref CreateCrossAccountRole, true] - + UpdateFulfillment: !Equals [!Ref UpdateFulfillmentURL, true] Resources: @@ -1078,14 +1086,25 @@ Resources: - 'sts:AssumeRole' ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - arn:aws:iam::aws:policy/AWSMarketplaceSellerFullAccess + Policies: + - PolicyName: manage-products + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - "aws-marketplace:StartChangeSet" + - "aws-marketplace:DescribeEntity" + Resource: + - !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/SaaSProduct/${ProductId}" + - !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/ChangeSet/*" - UpdateFulfillmentURL: + FulfillmentURL: Type: Custom::Lambda + Condition: UpdateFulfillment Properties: ServiceToken: !GetAtt UpdateFulfillmentURLCustomResource.Arn ProductId: !Ref ProductId - # FulfillmentUrl: 'https://cachicamo.org' FulfillmentUrl: !If [ CreateWeb, !Sub "https://${CloudfrontDistribution.DomainName}/redirectmarketplacetoken", @@ -1170,7 +1189,6 @@ Resources: } }; - Outputs: CrossAccountRole: @@ -1181,6 +1199,7 @@ Outputs: !GetAtt CrossAccountRoleForSaaSIntegration.Arn, "N/A" ] + WebsiteS3Bucket: Description: S3 bucket for hosting the static site. You can retrieve the files at https://github.com/aws-samples/aws-marketplace-serverless-saas-integration/tree/master/web. Value: From 88a12af72c682747c5729c8e0f94b7c446ffde6a Mon Sep 17 00:00:00 2001 From: Oscar Carrasquero Date: Thu, 12 Sep 2024 15:39:12 +0200 Subject: [PATCH 4/4] Updated Tests with new parameters --- buildspec.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/buildspec.yaml b/buildspec.yaml index c3ad5e6..09cb347 100644 --- a/buildspec.yaml +++ b/buildspec.yaml @@ -12,7 +12,7 @@ phases: - echo "${AWSMarketplaceMeteringRecordsTableName}" - echo "${MarketplaceTechAdminEmail}" - echo "${NewSubscribersTableName}" - - echo "${ProductCode}" + - echo "${ProductId}" - echo "${TypeOfSaaSListing}" - echo "${WebsiteS3BucketName}" - echo "${SNSAccountID}" @@ -21,6 +21,7 @@ phases: - echo "${CrossAccountId}" - echo "${CrossAccountRoleName}" - echo "${CreateRegistrationWebPage}" + - echo "${UpdateFulfillmentURL}" build: commands: - echo Build started @@ -32,7 +33,7 @@ phases: ParameterKey=WebsiteS3BucketName,ParameterValue=${WebsiteS3BucketName}-${CODEBUILD_BUILD_NUMBER} \ ParameterKey=NewSubscribersTableName,ParameterValue=${NewSubscribersTableName}-${CODEBUILD_BUILD_NUMBER} \ ParameterKey=AWSMarketplaceMeteringRecordsTableName,ParameterValue=${AWSMarketplaceMeteringRecordsTableName}-${CODEBUILD_BUILD_NUMBER} \ - ParameterKey=ProductCode,ParameterValue=${ProductCode} \ + ParameterKey=ProductId,ParameterValue=${ProductId} \ ParameterKey=MarketplaceTechAdminEmail,ParameterValue=${MarketplaceTechAdminEmail} \ ParameterKey=MarketplaceSellerEmail,ParameterValue=${MarketplaceTechAdminEmail} \ ParameterKey=SNSAccountID,ParameterValue=${SNSAccountID} \ @@ -40,7 +41,8 @@ phases: ParameterKey=CreateCrossAccountRole,ParameterValue=${CreateCrossAccountRole} \ ParameterKey=CrossAccountId,ParameterValue=${CrossAccountId} \ ParameterKey=CrossAccountRoleName,ParameterValue=${CrossAccountRoleName}-${CODEBUILD_BUILD_NUMBER} \ - ParameterKey=CreateRegistrationWebPage,ParameterValue=${CreateRegistrationWebPage} + ParameterKey=CreateRegistrationWebPage,ParameterValue=${CreateRegistrationWebPage} \ + ParameterKey=UpdateFulfillmentURL,ParameterValue=${UpdateFulfillmentURL} post_build: commands: - echo Build completed