mirror of
https://github.com/quay/quay.git
synced 2025-04-18 10:44:06 +03:00
546 lines
19 KiB
Python
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)
|