Integrated SaaS solutions on GCP Marketplace are software solutions that run on your infrastructure, regardless of location, but are billed by Google.

In this codelab, you will set up a basic Integrated SaaS solution that integrates with GCP Marketplace to:

To install the python modules, use the following command:

pip install --upgrade google-api-python-client google-cloud google-cloud-pubsub
git clone https://github.com/googlecodelabs/gcp-marketplace-integrated-saas.git
cd gcp-marketplace-integrated-saas

Download ZIP

Next, you will set up the backend for the sample solution.

At a high level, you integrate the sample solution with GCP Marketplace in the following ways:

When a user chooses a subscription plan, you get a notification from GCP Marketplace through a Cloud Pub/Sub topic.

To listen to messages on a Cloud Pub/Sub topic, you must first create a subscription.

To create a subscription, use the create_subscription.py script:

cd gcp-marketplace-integrated-saas/impl/step_1_pubsub
python create_subscription.py codelab-project

Where codelab-project is the project that you created for this codelab.

To see the subscription, open the Cloud Pub/Sub dashboard in the GCP console:

https://console.cloud.google.com/cloudpubsub/subscriptions

Try a test subscription request

To test this sample app as a user, open the codelab product in Marketplace, make sure that you have your codelab project selected, then choose a subscription plan.

To see the Cloud Pub/Sub messages that are sent when you choose a plan, navigate to the root of the repository, and run the following command:

~/gcp-marketplace-integrated-saas$ python -m impl.step_1_pubsub.app codelab-project

Where codelab-project is the project that you created for this codelab.

To see the sample code that listens for Cloud Pub/Sub messages, see https://github.com/googlecodelabs/gcp-marketplace-integrated-saas/blob/master/impl/step_1_pubsub/app.py.

In the Cloud Pub/Sub message, the eventType field shows why the message was sent. When you choose a plan, you should see a message for eventType: ENTITLEMENT_CREATION_REQUESTED, which represents your earlier choice of subscription plan.

If you cancel your plan while this script is running, you'll see a new message, with the eventType: ENTITLEMENT_CANCELLED.

Note that the above sample does not acknowledge messages. This allows you to more easily test by receiving the same messages each time you run your application.

To close the script, press CTRL + \.

Now that you can receive messages from GCP Marketplace, you must start handling the resources that the GCP Marketplace Procurement service creates on behalf of the customer.

The first is the account resource. An account represents a customer's connection to your product. You must store the customer's Procurement account ID in your database, to map the relationship between their Google account, and their account in your service.

When a customer chooses a plan, GCP Marketplace sends a Cloud Pub/Sub notification that the customer is requesting an account. Your application must approve the request. In this codelab, you approve the account requests when the Cloud Pub/Sub messages are received.

Create the database for the account information

For this codelab, we use a simple JSON database that can keep track of customer accounts and purchases.

To test this sample, create gcp-marketplace-integrated-saas/impl/database/database.json, and add an empty JSON object:

{}

The module that reads and writes the database is in impl/database.

The sample implementation uses a schema that can be extended if you are integrating more than one product offering with GCP Marketplace. The following is an example database entry for a user who subscribed to the Very Good plan in the sample application:

{
   "a2b3c4d5-b3f1-4dea-b134-generated_id":{
      "procurement_account_id":"generated-b3f1-4dea-b134-4a1d100c0335",
      "internal_account_id":"generated-45b7-4f4d-1bcd-2abb114f77de",
      "products":{
         "isaas-codelab":{
            "start_time":"2019-01-04T01:21:16.188Z",
            "plan_id":"very-good",
            "product_id":"isaas-codelab",
            "consumer_id":"project_number:123123345345"
         }
      }
   }
}

In your final implementation, you must connect your application with your own databases to link customers' GCP Marketplace accounts with your own customer resources.

Approving the account

To approve the account request, run the following command.

~/gcp-marketplace-integrated-saas$ python -m impl.step_2_account.app [YOUR_PROJECT_ID]

The sample code to approve an account is in impl/step_2_account.

The sample implementation uses the Procurement class, which handles the interactions with the Procurement API. Here are its get_account() and approve_account() methods:

step_2_account/app.py

def _get_account_name(self, account_id):
    return 'providers/DEMO-{}/accounts/{}'.format(self.project_id,
                                                  account_id)

def get_account(self, account_id):
    """Gets an account from the Procurement Service."""
    name = self._get_account_name(account_id)
    request = self.service.providers().accounts().get(name=name)
    try:
        response = request.execute()
        return response
    except HttpError as err:
        if err.resp.status == 404:
            return None

def approve_account(self, account_id):
    """Approves the account in the Procurement Service."""
    name = self._get_account_name(account_id)
    request = self.service.providers().accounts().approve(
        name=name, body={'approvalName': 'signup'})
    request.execute()

For this codelab, in the Procurement service, the provider ID is DEMO-codelab-project, where codelab-project is the project you created. While interacting with the Procurement API, the account name must use the following format:

providers/DEMO-codelab-project/accounts/account-id

Next, you approve an entitlement, which is a record of the customer's purchase.

When a customer chooses a subscription plan in GCP Marketplace, an account is created, and then a new entitlement request is created immediately. The entitlement represents the purchase of a service. Before the customer can start using the service, you must approve the entitlement request, and then set up the service for the customer to start using it.

When the sample application gets a Cloud Pub/Sub message with the eventType ENTITLEMENT_CREATION_REQUESTED, the entitlement is approved, and the application must wait for an ENTITLEMENT_ACTIVE message to record the entitlement in the database, then set up the resources for the customer.

To create the entitlement, run the following command:

~/gcp-marketplace-integrated-saas$ python -m impl.step_3_entitlement_create.app codelab-project

The code to approve the entitlement is in the sample implementation.

Next, you handle situations where a customer requests a change to their subscription plan.

If your service has multiple plans, you must handle requests from customers who might want to upgrade or downgrade their existing plan.

If your service has only one plan, skip to Handle cancelled purchases.

There's no technical difference between an entitlement becoming active for the first time and becoming active after a plan change. For this reason, the sample implementation has a shared handleActiveEntitlement() method for both cases. This method checks incoming messages for entitlement-related events:

step_4_entitlement_change/app.py

def handleActiveEntitlement(self, entitlement, customer, accountId):
  """Updates the database to match the active entitlement."""

  product = {
      'product_id': entitlement['product'],
      'plan_id': entitlement['plan'],
  }

  if 'consumerId' in entitlement:
    product['consumer_id'] = entitlement['consumerId']

  customer['products'][entitlement['product']] = product

  self.db.write(accountId, customer)

The following snippet checks whether the eventType is ENTITLEMENT_PLAN_CHANGE_REQUESTED or ENTITLEMENT_PLAN_CHANGED.

step_4_entitlement_change/app.py

elif eventType == 'ENTITLEMENT_PLAN_CHANGE_REQUESTED':
  if state == 'ENTITLEMENT_PENDING_PLAN_CHANGE_APPROVAL':
    # Don't write anything to our database until the entitlement becomes
    # active within the Procurement Service.
    self.approveEntitlementPlanChange(id, entitlement['newPendingPlan'])
    return True

elif eventType == 'ENTITLEMENT_PLAN_CHANGED':
  if state == 'ENTITLEMENT_ACTIVE':
    # Handle an active entitlement after a plan change.
    self.handleActiveEntitlement(entitlement, customer, accountId)
    return True

In your final implementation, when the entitlement transitions back to the ENTITLEMENT_ACTIVE state, your listener method should update your database to reflect the change and do any necessary provisioning.

Depending on how you set up your product with your Partner Engineer, your service might not allow downgrades or cancellations until the end of a billing cycle. In such cases, the plan change will continue to be pending even after the approval, but the entitlement will not go back to the ENTITLEMENT_ACTIVE state until the plan change completes.

For the code that checks for and approves entitlement changes, see the sample implementation.

Next, you handle situations where customers cancel their purchases.

Customers may choose to cancel their purchases. Depending on how you set up the product with your Partner Engineer, the cancellation can take effect immediately, or at the end of the billing cycle.

When a customer cancels their purchase, a message with the eventType ENTITLEMENT_PENDING_CANCELLATION is sent. If you've set up your product to process cancellations immediately, a message with the eventType ENTITLEMENT_CANCELLED is sent soon after.

step_6_entitlement_cancel/app.py

elif eventType == 'ENTITLEMENT_CANCELLED':
  # Clear out our records of the customer's plan.
  if entitlement['product'] in customer['products']:
    del customer['products'][entitlement['product']]

  ### TODO: Turn off customer's service. ###
  self.db.write(accountId, customer)
  return True

elif eventType == 'ENTITLEMENT_PENDING_CANCELLATION':
  # Do nothing. We want to cancel once it's truly canceled. For now it's
  # just set to not renew at the end of the billing cycle.
  return True

elif eventType == 'ENTITLEMENT_CANCELLATION_REVERTED':
  # Do nothing. The service was already active, but now it's set to renew
  # automatically at the end of the billing cycle.
  return True

Your service must wait for the ENTITLEMENT_CANCELLED message to then remove the entitlement from your database, and turn off the service for the customer.

After the entitlement is cancelled, it is deleted from Google's systems, and a message with the eventType ENTITLEMENT_DELETED is sent:

step_6_entitlement_cancel/app.py

elif eventType == 'ENTITLEMENT_DELETED':
  # Do nothing. Entitlements can only be deleted when they are already
  # cancelled, so our state is already up-to-date.
  return True

For the code that cancels entitlement, see the sample implementation.

Some services have usage-based components, where Google needs to know about customers' usage of the service to charge the customer the correct amount. Your service must report usage through the Google Service Control API.

If your service does not have usage-based components, skip this section.

For detailed information on sending usage reports, see the onboarding documentation.

Usage reports should be sent to the Google Service Control API hourly. In this codelab, the reports are sent using a script that you could schedule as a cron job. The script stores the time of the last usage report in the database, and uses that as the start time to measure usage.

The script checks each active customer of the service, and sends a usage report to Google Service Control using the customer entitlement's consumer_id field. The script then updates the database entry for the customer to have a last_report_time set to the end time of the usage report just sent.

Google Service Control exposes two methods: check and report. The former should always be called immediately before a call to the latter. If the former has any errors, the customer's service should be disabled until it is fixed.

The following snippet reports the usage for the demo app. For this codelab, the service_name is isaas-codelab.mp-marketplace-partner-demos.appspot.com.

service.services().report(
    serviceName=service_name, body={
        'operations': [operation]
    }).execute()
product['last_report_time'] = end_time
database.write(customer_id, customer)

See the sample implementation of this script for the full code. To make a sample usage report, run the following command:

python report.py prod isaas-codelab.mp-marketplace-partner-demos.appspot.com

You learned how your managed service can integrate with GCP Marketplace to handle customer accounts and entitlements, and to report usage against a service.

Clean up

If you no longer plan to use them, delete the following resources:

What's next

Integrate your frontend

The samples in this codelab automatically approve accounts and entitlements. In practice, your customers must be directed to a sign-up page that you create, where they can create accounts in your system. After they sign up successfully, you must make the API requests to approve their accounts and entitlements.

For information on integrating your application's frontend, see the GCP Marketplace documentation.

Learn more about selling managed services

For an overview of selling managed services on GCP Marketplace, see Selling managed services: Integrated SaaS.