1
0
mirror of https://github.com/quay/quay.git synced 2025-04-18 10:44:06 +03:00
quay/util/marketplace.py
Marcus Kok 5e2fbd986b
marketplace: free tier integration for reconciler (PROJQUAY-5698) (#3589)
free sku integration for reconciliation worker
2025-04-08 08:55:37 -04:00

546 lines
19 KiB
Python

import json
import logging
import time
import requests
from data.billing import RECONCILER_SKUS, RH_SKUS, get_plan_using_rh_sku
from data.model import entitlements, organization_skus
logger = logging.getLogger(__name__)
REQUEST_TIMEOUT = 20
MARKETPLACE_FILE = "/conf/stack/quay-marketplace-api.crt"
MARKETPLACE_SECRET = "/conf/stack/quay-marketplace-api.key"
class RedHatUserApi(object):
def __init__(self, app_config):
self.cert = (MARKETPLACE_FILE, MARKETPLACE_SECRET)
self.user_endpoint = app_config.get("ENTITLEMENT_RECONCILIATION_USER_ENDPOINT")
def get_account_number(self, user):
email = user.email
account_numbers = self.lookup_customer_id(email)
return account_numbers
def lookup_customer_id(self, email):
"""
Send request to internal api for customer id (web customer id)
"""
request_body_dict = {
"by": {"emailStartsWith": email},
"include": {
"accountRelationships": [
{
"allOf": [
"primary_email",
"is_supportable",
"account_details",
],
"by": {"active": True},
}
]
},
}
request_url = f"{self.user_endpoint}/v2/findUsers"
try:
r = requests.request(
method="post",
url=request_url,
cert=self.cert,
json=request_body_dict,
verify=True,
timeout=REQUEST_TIMEOUT,
)
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.user_endpoint)
return None
info = json.loads(r.content)
if not info:
logger.debug("request to %s did not return any data", self.user_endpoint)
return None
customer_ids = []
for account in info:
customer_id = account["accountRelationships"][0]["account"].get("id")
# convert str response from api to int value
if customer_id.isdigit():
customer_id = int(customer_id)
customer_ids.append(customer_id)
return customer_ids
class RedHatSubscriptionApi(object):
def __init__(self, app_config):
self.cert = (MARKETPLACE_FILE, MARKETPLACE_SECRET)
self.marketplace_endpoint = app_config.get(
"ENTITLEMENT_RECONCILIATION_MARKETPLACE_ENDPOINT"
)
def lookup_subscription(self, webCustomerId, skuId):
"""
Use internal marketplace API to find subscription for customerId and sku
"""
logger.debug(
"looking up subscription sku %s for account %s", str(skuId), str(webCustomerId)
)
subscriptions_url = f"{self.marketplace_endpoint}/subscription/v5/search/criteria;sku={skuId};web_customer_id={webCustomerId}"
request_headers = {"Content-Type": "application/json"}
# Using CustomerID to get active subscription for user
try:
r = requests.request(
method="get",
url=subscriptions_url,
headers=request_headers,
cert=self.cert,
verify=True,
timeout=REQUEST_TIMEOUT,
)
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.marketplace_endpoint)
return None
try:
subscriptions = json.loads(r.content)
except json.decoder.JSONDecodeError:
return None
valid_subscriptions = []
if subscriptions:
for subscription in subscriptions:
end_date = subscription["effectiveEndDate"]
now_ms = time.time() * 1000
# Is subscription still valid?
if now_ms < end_date:
logger.debug("subscription found for %s", str(skuId))
valid_subscriptions.append(subscription)
return valid_subscriptions if len(valid_subscriptions) > 0 else None
return None
def extend_subscription(self, subscription_id, endDate):
"""
Use internal marketplace API to extend a subscription to endDate
"""
extend_url = f"{self.marketplace_endpoint}/subscription/v5/extendActiveSubscription/{subscription_id}/{endDate}"
request_headers = {"Content-Type:": "application/json"}
try:
r = requests.request(
method="get",
url=extend_url,
headers=request_headers,
cert=self.cert,
verify=True,
timeout=REQUEST_TIMEOUT,
)
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.marketplace_endpoint)
return None
logger.debug("Extended subscription %i to %s", subscription_id, str(endDate))
return r
def create_entitlement(self, customerId, sku):
"""
create subscription for user in internal marketplace
"""
request_url = f"{self.marketplace_endpoint}/subscription/v5/createPerm"
request_headers = {"Content-Type": "application/json"}
logger.debug("Creating subscription for %s with sku %s", customerId, sku)
request_body_dict = {
"sku": sku,
"qty": 1,
"duration": {
"hour": 0,
"day": 0,
"week": 0,
"month": 1,
"year": 0,
"minute": 0,
"second": 0,
"zero": True,
"millisecond": 0,
},
"webCustomerId": customerId,
}
logger.debug("Created entitlement")
try:
r = requests.request(
method="post",
url=request_url,
cert=self.cert,
headers=request_headers,
json=request_body_dict,
verify=True,
timeout=REQUEST_TIMEOUT,
)
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.marketplace_endpoint)
return 408
return r.status_code
def remove_entitlement(self, subscription_id):
"""
Removes subscription from user given subscription_id
"""
request_url = (
f"{self.marketplace_endpoint}/subscription/v5/terminateSubscription/{subscription_id}"
)
request_headers = {"Content-Type": "application/json"}
logger.debug("Terminating subscription with id %s", subscription_id)
try:
r = requests.request(
method="post",
url=request_url,
cert=self.cert,
headers=request_headers,
verify=True,
timeout=REQUEST_TIMEOUT,
)
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.marketplace_endpoint)
return 408
return r.status_code
def get_subscription_details(self, subscription_id):
"""
Return the sku and expiration date for a specific subscription
"""
request_url = f"{self.marketplace_endpoint}/subscription/v5/products/subscription_id={subscription_id}"
request_headers = {"Content-Type": "application/json"}
try:
r = requests.request(
method="get",
url=request_url,
cert=self.cert,
verify=True,
timeout=REQUEST_TIMEOUT,
headers=request_headers,
)
info = json.loads(r.content)
subscription_sku = info[0]["sku"]
expiration_date = info[1]["activeEndDate"]
terminated_date = info[0]["terminatedDate"]
return {
"sku": subscription_sku,
"expiration_date": expiration_date,
"terminated_date": terminated_date,
}
except requests.exceptions.SSLError:
raise requests.exceptions.SSLError
except requests.exceptions.ReadTimeout:
logger.info("request to %s timed out", self.marketplace_endpoint)
return None
def get_list_of_subscriptions(
self, account_number, filter_out_org_bindings=False, convert_to_stripe_plans=False
):
"""
Returns a list of all active subscriptions a user has
in RH marketplace
"""
subscription_list = []
for sku in RH_SKUS:
subscriptions = self.lookup_subscription(account_number, sku)
if subscriptions:
for user_subscription in subscriptions:
if user_subscription is not None:
if (
user_subscription["masterEndSystemName"] == "SUBSCRIPTION"
and sku in RECONCILER_SKUS
):
continue
bound_to_org, bound_subs = organization_skus.subscription_bound_to_org(
user_subscription["id"]
)
if filter_out_org_bindings and bound_to_org:
# special case for MW02702, we need to calculate how many
# subscriptions are left if it is split across orgs
if sku == "MW02702":
total_attached = 0
for sub in bound_subs:
total_attached += sub["quantity"]
user_subscription["quantity"] -= total_attached
if user_subscription["quantity"] <= 0:
continue
else:
continue
if convert_to_stripe_plans:
quantity = user_subscription["quantity"]
for i in range(quantity):
subscription_list.append(get_plan_using_rh_sku(sku))
else:
# add in sku field for convenience
user_subscription["sku"] = sku
subscription_list.append(user_subscription)
return subscription_list
# Mocked classes for unit tests
TEST_USER = {
"account_number": 12345,
"email": "subscriptions@devtable.com",
"username": "subscription",
"subscriptions": [
{
"id": 12345678,
"masterEndSystemName": "Quay",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 2,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
},
{
"id": 11223344,
"masterEndSystemName": "Quay",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 1,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
},
],
"private_subscription": {
"id": 80808080,
"masterEndSystemName": "Quay",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 10,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
},
"reconciled_subscription": {
"id": 87654321,
"masterEndSystemName": "SUBSCRIPTION",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 1,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
},
"terminated_subscription": {
"id": 22222222,
"masterEndSystemName": "SUBSCRIPTION",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 1,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
},
}
STRIPE_USER = {"account_number": 11111, "email": "stripe_user@test.com", "username": "stripe_user"}
FREE_USER = {
"account_number": 23456,
"email": "free_user@test.com",
"username": "free_user",
}
PAID_USER = {
"account_number": 34567,
"email": "paid@test.com",
"username": "paid_user",
"subscriptions": [
{
"id": 12345678,
"masterEndSystemName": "Quay",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 1,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
}
],
"free_sku": [
{
"id": 56781234,
"masterEndSystemName": "Quay",
"createdEndSystemName": "SUBSCRIPTION",
"createdDate": 1675957362000,
"lastUpdateEndSystemName": "SUBSCRIPTION",
"lastUpdateDate": 1675957362000,
"installBaseStartDate": 1707368400000,
"installBaseEndDate": 1707368399000,
"webCustomerId": 123456,
"subscriptionNumber": "12399889",
"quantity": 1,
"effectiveStartDate": 1707368400000,
"effectiveEndDate": 3813177600000,
}
],
}
class FakeUserApi(RedHatUserApi):
"""
Fake class used for tests
"""
def lookup_customer_id(self, email):
if email == TEST_USER["email"]:
return [TEST_USER["account_number"]]
if email == PAID_USER["email"]:
return [PAID_USER["account_number"]]
if email == FREE_USER["email"]:
return [FREE_USER["account_number"]]
if email == STRIPE_USER["email"]:
return [STRIPE_USER["account_number"]]
return None
class FakeSubscriptionApi(RedHatSubscriptionApi):
"""
Fake class used for tests
"""
def __init__(self):
self.subscription_extended = False
self.subscription_created = False
def lookup_subscription(self, customer_id, sku_id):
if customer_id == TEST_USER["account_number"] and sku_id == "MW02701":
return TEST_USER["subscriptions"]
elif customer_id == TEST_USER["account_number"] and sku_id == "MW02702":
return [TEST_USER["private_subscription"]]
elif customer_id == TEST_USER["account_number"] and sku_id == "MW00584MO":
return [TEST_USER["reconciled_subscription"]]
elif customer_id == PAID_USER["account_number"] and sku_id == "MW02701":
return PAID_USER["subscriptions"]
elif customer_id == PAID_USER["account_number"] and sku_id == "MW04192":
return PAID_USER["free_sku"]
elif customer_id == FREE_USER["account_number"]:
return []
return None
def create_entitlement(self, customer_id, sku_id):
self.subscription_created = True
def remove_entitlement(self, subscription_id):
pass
def extend_subscription(self, subscription_id, end_date):
self.subscription_extended = True
def get_subscription_details(self, subscription_id):
valid_ids = [subscription["id"] for subscription in TEST_USER["subscriptions"]]
if subscription_id in valid_ids:
return {"sku": "MW02701", "expiration_date": 3813177600000, "terminated_date": None}
elif subscription_id == 80808080:
return {"sku": "MW02702", "expiration_date": 1645544830000, "terminated_date": None}
elif subscription_id == 87654321:
return {"sku": "MW00584MO", "expiration_date": 3813177600000, "terminated_date": None}
elif subscription_id == 22222222:
return {
"sku": "MW00584MO",
"expiration_date": 3813177600000,
"terminated_date": 1645544830000,
}
else:
return None
class MarketplaceUserApi(object):
def __init__(self, app=None):
self.app = app
if app is not None:
self.state = self.init_app(app)
else:
self.state = None
def init_app(self, app):
marketplace_enabled = app.config.get("FEATURE_RH_MARKETPLACE", False)
reconciler_enabled = app.config.get("ENTITLEMENT_RECONCILIATION", False)
use_rh_api = marketplace_enabled or reconciler_enabled
marketplace_user_api = FakeUserApi(app.config)
if use_rh_api and not app.config.get("TESTING"):
marketplace_user_api = RedHatUserApi(app.config)
app.extensions = getattr(app, "extensions", {})
app.extensions["marketplace_user_api"] = marketplace_user_api
return marketplace_user_api
def __getattr__(self, name):
return getattr(self.state, name, None)
class MarketplaceSubscriptionApi(object):
def __init__(self, app=None):
self.app = app
if app is not None:
self.state = self.init_app(app)
else:
self.state = None
def init_app(self, app):
reconciler_enabled = app.config.get("ENTITLEMENT_RECONCILIATION", False)
marketplace_enabled = app.config.get("FEATURE_RH_MARKETPLACE", False)
use_rh_api = marketplace_enabled or reconciler_enabled
marketplace_subscription_api = FakeSubscriptionApi()
if use_rh_api and not app.config.get("TESTING"):
marketplace_subscription_api = RedHatSubscriptionApi(app.config)
app.extensions = getattr(app, "extensions", {})
app.extensions["marketplace_subscription_api"] = marketplace_subscription_api
return marketplace_subscription_api
def __getattr__(self, name):
return getattr(self.state, name, None)