diff --git a/README.md b/README.md index 070a2e3..de84288 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ see https://github.com/3scale/3scale-operator/blob/master/doc/backend-reference. - ProxyConfigPromote - Applications - Methods +- ApplicationAuth Command to run integration unit tests: `pipenv run pytest --log-cli-level=10 -vvvv -s ./tests/integration/ |& tee x` diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8e96626..4071ded 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,7 @@ import os import secrets +import random +import string from distutils.util import strtobool import pytest @@ -9,7 +11,6 @@ import threescale_api import threescale_api_crd -from threescale_api.resources import Application from threescale_api_crd.resources import ( Service, @@ -24,6 +25,7 @@ ApplicationPlan, Proxy, PricingRule, + Application, ) load_dotenv() @@ -119,7 +121,7 @@ def apicast_http_client(application, proxy, ssl_verify): @pytest.fixture(scope="module") def service_params(): suffix = get_suffix() - return dict(name=f"test-{suffix}") + return {"name": f"test-{suffix}"} @pytest.fixture(scope="module") @@ -133,14 +135,14 @@ def service(service_params, api) -> Service: def account_params(): suffix = get_suffix() name = f"testacc{suffix}" - return dict( - name=name, - username=name, - org_name=name, - monthly_billing_enabled=False, - monthly_charging_enabled=False, - email=f"{name}@name.none", - ) + return { + "name": name, + "username": name, + "org_name": name, + "monthly_billing_enabled": False, + "monthly_charging_enabled": False, + "email": f"{name}@name.none", + } @pytest.fixture(scope="module") @@ -159,12 +161,12 @@ def acc_user(account): @pytest.fixture(scope="module") def acc_user2_params(account, acc_user): name = acc_user["username"] + "2" - return dict( - username=name, - email=f"{name}@name.none", - role="member", - account_name=account["name"], - ) + return { + "username": name, + "email": f"{name}@name.none", + "role": "member", + "account_name": account["name"], + } @pytest.fixture(scope="module") @@ -175,12 +177,12 @@ def acc_user2(account, acc_user, acc_user2_params): @pytest.fixture(scope="module") def application_plan_params(service) -> dict: suffix = get_suffix() - return dict( - name=f"test-{suffix}", - setup_fee="1.00", - state_event="publish", - cost_per_month="3.00", - ) + return { + "name": f"test-{suffix}", + "setup_fee": "1.00", + "state_event": "publish", + "cost_per_month": "3.00", + } @pytest.fixture(scope="module") @@ -194,13 +196,13 @@ def application_plan(api, service, application_plan_params) -> ApplicationPlan: def application_params(application_plan, service, account): suffix = get_suffix() name = f"test-{suffix}" - return dict( - name=name, - description=name, - plan_id=application_plan["id"], - service_id=service["id"], - account_id=account["id"], - ) + return { + "name": name, + "description": name, + "plan_id": application_plan["id"], + "service_id": service["id"], + "account_id": account["id"], + } @pytest.fixture(scope="module") @@ -210,6 +212,30 @@ def application(service, application_plan, application_params, account) -> Appli cleanup(resource) +@pytest.fixture(scope="module") +def app_key_params(account, application): + value = "".join( + random.choices( + string.ascii_uppercase + + string.digits + + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + k=100, + ) + ) + return { + "application_id": application["id"], + "account_id": account["id"], + "key": value, + } + + +@pytest.fixture(scope="module") +def app_key(application, app_key_params) -> threescale_api.resources.ApplicationKey: + resource = application.keys.create(params=app_key_params) + yield resource + cleanup(resource) + + @pytest.fixture(scope="module") def proxy(service) -> Proxy: return service.proxy.list() @@ -244,7 +270,7 @@ def metric_params(service): suffix = get_suffix() friendly_name = f"test-metric-{suffix}" name = f"{friendly_name}".replace("-", "_") - return dict(friendly_name=friendly_name, system_name=name, unit="count") + return {"friendly_name": friendly_name, "system_name": name, "unit": "count"} @pytest.fixture(scope="module") @@ -252,7 +278,7 @@ def backend_metric_params(): suffix = get_suffix() friendly_name = f"test-metric-{suffix}" name = f"{friendly_name}".replace("-", "") - return dict(friendly_name=friendly_name, system_name=name, unit="count") + return {"friendly_name": friendly_name, "system_name": name, "unit": "count"} @pytest.fixture(scope="module") @@ -284,7 +310,7 @@ def method_params(): suffix = get_suffix() friendly_name = f"test-method-{suffix}" system_name = f"{friendly_name}".replace("-", "_") - return dict(friendly_name=friendly_name, system_name=system_name) + return {"friendly_name": friendly_name, "system_name": system_name} # 'friendly_name' is id in CRD for methods @@ -321,7 +347,12 @@ def mapping_rule_params(service): Fixture for getting paramteres for mapping rule for product/service. """ hits_metric = service.metrics.read_by_name("hits") - return dict(http_method="GET", pattern="/get", metric_id=hits_metric["id"], delta=1) + return { + "http_method": "GET", + "pattern": "/get", + "metric_id": hits_metric["id"], + "delta": 1, + } @pytest.fixture(scope="module") @@ -330,7 +361,12 @@ def backend_mapping_rule_params(backend, backend_metric): Fixture for getting paramteres for mapping rule for backend. """ back = backend_metric["id"] - return dict(http_method="GET", pattern="/anything/get/ida", metric_id=back, delta=1) + return { + "http_method": "GET", + "pattern": "/anything/get/ida", + "metric_id": back, + "delta": 1, + } @pytest.fixture(scope="module") @@ -388,13 +424,13 @@ def create_mapping_rule(service): rules = [] def _create(metric, http_method, path): - params = dict( - service_id=service["id"], - http_method=http_method, - pattern=f"/anything{path}", - delta=1, - metric_id=metric["id"], - ) + params = { + "service_id": service["id"], + "http_method": http_method, + "pattern": f"/anything{path}", + "delta": 1, + "metric_id": metric["id"], + } rule = service.mapping_rules.create(params=params) rules.append(rule) return rule @@ -415,13 +451,13 @@ def create_backend_mapping_rule(backend): rules = [] def _create(backend_metric, http_method, path): - params = dict( - backend_id=backend["id"], - http_method=http_method, - pattern=f"/anything{path}", - delta=1, - metric_id=backend_metric["id"], - ) + params = { + "backend_id": backend["id"], + "http_method": http_method, + "pattern": f"/anything{path}", + "delta": 1, + "metric_id": backend_metric["id"], + } rule = backend.mapping_rules.create(params=params) rules.append(rule) return rule @@ -439,9 +475,11 @@ def backend_params(api_backend): Fixture for getting backend parameters. """ suffix = get_suffix() - return dict( - name=f"test-backend-{suffix}", private_endpoint=api_backend, description="111" - ) + return { + "name": f"test-backend-{suffix}", + "private_endpoint": api_backend, + "description": "111", + } @pytest.fixture(scope="module") @@ -479,12 +517,12 @@ def tenant_params(): """ Params for custom tenant """ - return dict( - username=f"tenant{get_suffix()}", - admin_password="123456", - email=f"e{get_suffix()}@invalid.invalid", - org_name="org", - ) + return { + "username": f"tenant{get_suffix()}", + "admin_password": "123456", + "email": f"e{get_suffix()}@invalid.invalid", + "org_name": "org", + } @pytest.fixture(scope="module") @@ -499,7 +537,7 @@ def active_docs_params(active_docs_body): suffix = get_suffix() name = f"test-{suffix}" des = f"description-{suffix}" - return dict(name=name, body=active_docs_body, description=des) + return {"name": name, "body": active_docs_body, "description": des} @pytest.fixture(scope="module") @@ -518,17 +556,17 @@ def active_doc(api, service, active_docs_params) -> ActiveDoc: def openapi_params(active_docs_body): suffix = get_suffix() name = f"test-{suffix}" - params = dict( - name=name, - productionPublicBaseURL="http://productionPublicBaseURL", - stagingPublicBaseURL="http://stagingPublicBaseURL", - productSystemName="PrOdUcTsYsTeMnAmE", - privateBaseURL="http://privateBaseURL", - prefixMatching=True, - privateAPIHostHeader="privateAPIHostHeader", - privateAPISecretToken="privateAPISecretToken", - body=active_docs_body, - ) + params = { + "name": name, + "productionPublicBaseURL": "http://productionPublicBaseURL", + "stagingPublicBaseURL": "http://stagingPublicBaseURL", + "productSystemName": "PrOdUcTsYsTeMnAmE", + "privateBaseURL": "http://privateBaseURL", + "prefixMatching": True, + "privateAPIHostHeader": "privateAPIHostHeader", + "privateAPISecretToken": "privateAPISecretToken", + "body": active_docs_body, + } return params @@ -584,7 +622,7 @@ def policy_registry_params(policy_registry_schema): """Params for policy registry.""" suffix = get_suffix() name = f"test-{suffix}" - return dict(name=name, version="0.1", schema=policy_registry_schema) + return {"name": name, "version": "0.1", "schema": policy_registry_schema} @pytest.fixture(scope="module") @@ -601,7 +639,7 @@ def policy_registry(api, policy_registry_params) -> PolicyRegistry: @pytest.fixture(scope="module") def limit_params(metric): """Params for limit.""" - return dict(metric_id=metric["id"], period="minute", value=10) + return {"metric_id": metric["id"], "period": "minute", "value": 10} @pytest.fixture(scope="module") @@ -617,7 +655,7 @@ def limit(service, application, application_plan, metric, limit_params) -> Limit @pytest.fixture(scope="module") def backend_limit_params(backend_metric): """Params for limit.""" - return dict(metric_id=backend_metric["id"], period="minute", value=10) + return {"metric_id": backend_metric["id"], "period": "minute", "value": 10} @pytest.fixture(scope="module") @@ -640,7 +678,7 @@ def backend_limit( @pytest.fixture(scope="module") def prule_params(metric): """Params for prule.""" - return dict(metric_id=metric["id"], min=10, max=100, cost_per_unit="10") + return {"metric_id": metric["id"], "min": 10, "max": 100, "cost_per_unit": "10"} @pytest.fixture(scope="module") @@ -656,7 +694,12 @@ def prule(service, application, application_plan, metric, prule_params) -> Prici @pytest.fixture(scope="module") def backend_prule_params(backend_metric): """Params for prule.""" - return dict(metric_id=backend_metric["id"], min=10, max=100, cost_per_unit=10) + return { + "metric_id": backend_metric["id"], + "min": 10, + "max": 100, + "cost_per_unit": 10, + } @pytest.fixture(scope="module") @@ -683,7 +726,7 @@ def promote_params(api, service): """ Promote params for service. """ - return dict(productCRName=service.crd.as_dict()["metadata"]["name"]) + return {"productCRName": service.crd.as_dict()["metadata"]["name"]} @pytest.fixture(scope="module") diff --git a/tests/integration/test_integration_application.py b/tests/integration/test_integration_application.py index 9495c25..e0908fe 100644 --- a/tests/integration/test_integration_application.py +++ b/tests/integration/test_integration_application.py @@ -1,5 +1,7 @@ -import pytest import secrets +import random +import string +import pytest from tests.integration import asserts @@ -36,12 +38,12 @@ def test_application_can_be_read_by_name(api, application_params, application): @pytest.fixture(scope="module") def application_plan_params2(): suffix = secrets.token_urlsafe(8) - return dict( - name=f"test-{suffix}", - setup_fee="1.00", - state_event="publish", - cost_per_month="3.00", - ) + return { + "name": f"test-{suffix}", + "setup_fee": "1.00", + "state_event": "publish", + "cost_per_month": "3.00", + } @pytest.fixture(scope="module") @@ -55,7 +57,7 @@ def application_plan2(service, application_plan_params2): def update_application_params(application_plan2): suffix = secrets.token_urlsafe(8) name = f"updated-{suffix}" - return dict(name=name, description=name, plan_id=application_plan2["id"]) + return {"name": name, "description": name, "plan_id": application_plan2["id"]} def test_application_update(update_application_params, application): @@ -74,3 +76,38 @@ def test_application_set_state(application): assert application["state"] == "suspended" application = application.set_state("resume") assert application["state"] == "live" + + +# Application Atuhentication - Application keys + + +def test_application_key_list(application, app_key): + keys = application.keys.list() + assert len(keys) > 0 + + +def test_application_key_can_be_created(app_key, app_key_params): + asserts.assert_resource(app_key) + asserts.assert_resource_params(app_key, app_key_params) + + +def test_application_autogenerated_key_can_be_created(application, app_key_params): + keys_len = len(application.keys.list()) + key_params = app_key_params.copy() + key_params.pop("key", None) + key_params["generateSecret"] = True + new_key = application.keys.create(params=key_params) + asserts.assert_resource(new_key) + asserts.assert_resource_params(new_key, key_params) + assert new_key["value"] + assert len(application.keys.list()) == keys_len + 1 + + +def test_application_update_userkey(application): + new_key = "".join( + random.choices(string.ascii_letters + string.digits + "-_.", k=100) + ) + application.update(params={"user_key": new_key}) + application.read() + asserts.assert_resource(application) + assert application["user_key"] == new_key diff --git a/threescale_api_crd/client.py b/threescale_api_crd/client.py index 52f6826..8fe14dd 100644 --- a/threescale_api_crd/client.py +++ b/threescale_api_crd/client.py @@ -13,7 +13,9 @@ class ThreeScaleClientCRD(threescale_api.client.ThreeScaleClient): Threescale client for CRD. """ - def __init__(self, url, token, ocp_provider_ref=None, ocp_namespace=None, *args, **kwargs): + def __init__( + self, url, token, ocp_provider_ref=None, ocp_namespace=None, *args, **kwargs + ): super().__init__(url, token, *args, **kwargs) self._ocp_provider_ref = ocp_provider_ref self._ocp_namespace = ThreeScaleClientCRD.get_namespace(ocp_namespace) @@ -45,9 +47,12 @@ def __init__(self, url, token, ocp_provider_ref=None, ocp_namespace=None, *args, self._applications = resources.Applications( parent=self, account=None, instance_klass=resources.Application ) + self._app_auths = resources.AppAuths( + parent=self, instance_klass=resources.AppAuth + ) @classmethod - def get_namespace(_ignore, namespace): + def get_namespace(cls, namespace): """ Returns namespace. If there is no valid Openshift 'oc' session, returns "NOT LOGGED IN". """ @@ -126,6 +131,13 @@ def applications(self) -> resources.Applications: """ return self._applications + @property + def app_auths(self) -> resources.AppAuths: + """Gets Application Auth client + Returns(resources.Auths): Application Auth client + """ + return self._app_auths + @property def ocp_provider_ref(self): """Gets provider reference""" diff --git a/threescale_api_crd/constants.py b/threescale_api_crd/constants.py index 33ce629..246cc50 100644 --- a/threescale_api_crd/constants.py +++ b/threescale_api_crd/constants.py @@ -7,7 +7,8 @@ SERVICE_AUTH_DEFS = { "1": { "userkey": { - "authUserKey": "token", + # "token", see https://issues.redhat.com/browse/THREESCALE-11072 + "authUserKey": "user_key", "credentials": "authorization", "gatewayResponse": {}, }, @@ -57,7 +58,8 @@ "apicastHosted": { "authentication": { "userkey": { - "authUserKey": "token", + # "token", see https://issues.redhat.com/browse/THREESCALE-11072 + "authUserKey": "user_key", "credentials": "query", "gatewayResponse": {}, }, @@ -309,6 +311,26 @@ }, } +SPEC_APP_AUTH = { + "apiVersion": "capabilities.3scale.net/v1beta1", + "kind": "ApplicationAuth", + "metadata": { + "name": None, + "namespace": None, + "annotations": {"insecure_skip_verify": "true"}, + }, + "spec": { + "providerAccountRef": { + "name": None, + }, + "applicationCRName": None, + "generateSecret": False, + "authSecretRef": { + "name": None, + }, + }, +} + SPEC_APPLICATION = { "apiVersion": "capabilities.3scale.net/v1beta1", "kind": "Application", @@ -534,6 +556,12 @@ "deleteCR": "deleteCR", } +KEYS_APP_AUTH = { + "applicationCRName": "applicationCRName", + "generateSecret": "generateSecret", + "authSecretRef": "authSecretRef", +} + KEYS_APPLICATION = { "description": "description", "name": "name", diff --git a/threescale_api_crd/resources.py b/threescale_api_crd/resources.py index 175c4ce..b6342b2 100644 --- a/threescale_api_crd/resources.py +++ b/threescale_api_crd/resources.py @@ -8,10 +8,12 @@ import os import secrets import random +import time +from urllib.parse import quote_plus + import yaml import requests import openshift_client as ocp -import time import threescale_api import threescale_api.resources @@ -178,7 +180,9 @@ def update(self, *args, **kwargs): def delete(self): """This functions is not implemented for Proxies.""" - raise threescale_api.errors.ThreeScaleApiError("Delete not implemented for Proxies") + raise threescale_api.errors.ThreeScaleApiError( + "Delete not implemented for Proxies" + ) def deploy(self): """ @@ -366,8 +370,7 @@ def trans_item(self, key, value, obj): return obj[key][0] met = self.parent.metrics.read(int(obj[key])) return met["system_name"].split(".")[0] - else: - return obj[key] + return obj[key] @staticmethod def insert_into_position(maps, params, spec): @@ -393,7 +396,7 @@ def get_from_position(maps, params): if "last" in params.keys(): return maps[-1] for mapi in maps: - if all([params[key] == mapi[key] for key in params.keys()]): + if all(params[key] == mapi[key] for key in params.keys()): return mapi return None @@ -595,7 +598,7 @@ def in_create(self, maps, params, spec): self.parent.read() self.parent.update({"metrics": maps}) for mapi in self.get_list(): - if all([params[key] == mapi[key] for key in params.keys()]): + if all(params[key] == mapi[key] for key in params.keys()): return mapi return None @@ -714,7 +717,7 @@ def in_create(self, maps, params, spec): self.parent.update({"backend_usages": maps}) params.pop("name", None) for mapi in self.get_list(): - if all([params[key] == mapi[key] for key in params.keys()]): + if all(params[key] == mapi[key] for key in params.keys()): return mapi return None @@ -924,7 +927,7 @@ def in_create(self, maps, params, spec): self.parent.read() self.parent.update({"application_plans": maps}) for mapi in self.get_list(): - if all([params[key] == mapi[key] for key in params.keys()]): + if all(params[key] == mapi[key] for key in params.keys()): return mapi return None @@ -1139,6 +1142,7 @@ class Promotes(DefaultClientCRD, threescale_api.defaults.DefaultClient): SELECTOR = "ProxyConfigPromote" ID_NAME = "productId" # flake8: noqa E501 + # pylint: disable=line-too-long ERROR_MSG = '[]: Invalid value: "": cannot promote to staging as no product changes detected. Delete this proxyConfigPromote CR, then introduce changes to configuration, and then create a new proxyConfigPromote CR' def __init__( @@ -1176,12 +1180,67 @@ def _is_ready(self, obj): if not state["Failed"] and state["Ready"]: return True - else: - # extract message for 'Failed' - msg = [st["message"] for st in conds if st["type"] == "Failed"] - if msg and msg[0] == Promotes.ERROR_MSG: - return True + + # extract message for 'Failed' + msg = [st["message"] for st in conds if st["type"] == "Failed"] + if msg and msg[0] == Promotes.ERROR_MSG: + return True + return False + + +class AppAuths(DefaultClientCRD, threescale_api.defaults.DefaultClient): + """ + CRD client for Application Auths. This class is only implemented in CRD and not in 3scale API. + """ + + CRD_IMPLEMENTED = True + SPEC = constants.SPEC_APP_AUTH + KEYS = constants.KEYS_APP_AUTH + SELECTOR = "ApplicationAuth" + ID_NAME = "" + # flake8: noqa E501 + READY_MSG = "Application authentication has been successfully pushed, any further interactions with this CR will not be applied" + + def __init__( + self, + parent, + *args, + entity_name="", + entity_collection="app_auths", + **kwargs, + ): + super().__init__( + *args, + parent=parent, + entity_name=entity_name, + entity_collection=entity_collection, + **kwargs, + ) + + def before_create(self, params, spec): + """Called before create.""" + + def before_update(self, new_params, resource): + """Called before update.""" + pass + + def _is_ready(self, obj): + """Is object ready?""" + if not ("status" in obj.model and "conditions" in obj.model.status): return False + state = {"Failed": True, "Ready": False} + conds = obj.as_dict()["status"]["conditions"] + for sta in conds: + state[sta["type"]] = sta["status"] == "True" + + if not state["Failed"] and state["Ready"]: + return True + + # extract message for 'Failed' + msg = [st["message"] for st in conds if st["type"] == "Failed"] + if msg and msg[0] == AppAuths.READY_MSG: + return True + return False class Tenants(DefaultClientCRD, threescale_api.resources.Tenants): @@ -1451,15 +1510,13 @@ def _create_instance_trans(self, instance): if self.metric[BackendMetrics.ID_NAME] == obj["metric_name"] and self.metric.parent["system_name"] == obj["backend_name"] ] - else: - return [ - obj - for obj in instance - if self.metric[Metrics.ID_NAME] == obj["metric_name"] - and "backend_name" not in obj.entity - ] - else: - return [obj for obj in instance] + return [ + obj + for obj in instance + if self.metric[Metrics.ID_NAME] == obj["metric_name"] + and "backend_name" not in obj.entity + ] + return [obj for obj in instance] class PricingRules(DefaultClientNestedCRD, threescale_api.resources.PricingRules): @@ -1650,15 +1707,13 @@ def _create_instance_trans(self, instance): if self.metric[Metrics.ID_NAME] == obj["metric_name"] and self.metric.parent["system_name"] == obj["backend_name"] ] - else: - return [ - obj - for obj in instance - if self.metric[Metrics.ID_NAME] == obj["metric_name"] - and "backend_name" not in obj.entity - ] - else: - return [obj for obj in instance] + return [ + obj + for obj in instance + if self.metric[Metrics.ID_NAME] == obj["metric_name"] + and "backend_name" not in obj.entity + ] + return [obj for obj in instance] class Applications(DefaultClientCRD, threescale_api.resources.Applications): @@ -1712,7 +1767,37 @@ def before_create(self, params, spec): def before_update(self, new_params, resource): """Called before update.""" - pass + new_user_key = new_params.pop("user_key", None) + if new_user_key: + app_auth_params = {} + app_auth_params["applicationCRName"] = resource["name"] + secret_name = "keysec" + "".join( + random.choice(string.ascii_lowercase) for _ in range(5) + ) + + spec_sec = copy.deepcopy(constants.SPEC_SECRET) + spec_sec["metadata"]["name"] = secret_name + spec_sec["metadata"]["namespace"] = self.threescale_client.ocp_namespace + + if new_params.pop("generateSecret", None): + spec_sec["data"]["UserKey"] = "" + app_auth_params["generateSecret"] = True + else: + app_auth_params["generateSecret"] = False + + key_ascii = str(new_user_key).encode("ascii") + key_enc = base64.b64encode(key_ascii) + + spec_sec["data"]["UserKey"] = key_enc.decode("ascii") + + result = ocp.create(spec_sec) + assert result.status() == 0 + + app_auth_params["authSecretRef"] = {"name": secret_name} + + app_auth = self.threescale_client.app_auths.create(params=app_auth_params) + app_auth.delete() + result.delete() def _is_ready(self, obj): """Is object ready?""" @@ -1726,8 +1811,59 @@ def trans_item(self, key, value, obj): """Translate entity to CRD.""" if key in ["service_name", "account_name"]: return {"name": obj[key]} + return obj[key] + + +class ApplicationKeys(threescale_api.resources.ApplicationKeys): + """Application Keys class""" + + def __init__(self, *args, entity_name="key", entity_collection="keys", **kwargs): + super().__init__( + *args, + entity_name=entity_name, + entity_collection=entity_collection, + **kwargs, + ) + + def create( + self, params: dict = None, **kwargs + ) -> "threescale_api.resources.ApplicationKey": + """Create a new instance of ApplicationKey via AppAuth instance.""" + params.pop("application_id", None) + params.pop("account_id", None) + + params["applicationCRName"] = self.parent["name"] + secret_name = "keysec" + "".join( + random.choice(string.ascii_lowercase) for _ in range(5) + ) + + spec_sec = copy.deepcopy(constants.SPEC_SECRET) + spec_sec["metadata"]["name"] = secret_name + spec_sec["metadata"]["namespace"] = self.threescale_client.ocp_namespace + + if "generateSecret" in params and params["generateSecret"]: + spec_sec["data"]["ApplicationKey"] = "" else: - return obj[key] + params["generateSecret"] = False + + key = params.pop("key") + key_ascii = str(key).encode("ascii") + key_enc = base64.b64encode(key_ascii) + + spec_sec["data"]["ApplicationKey"] = key_enc.decode("ascii") + + result = ocp.create(spec_sec) + assert result.status() == 0 + + params["authSecretRef"] = {"name": secret_name} + + app_auth = self.threescale_client.app_auths.create(params=params, **kwargs) + app_auth.delete() + result.delete() + key_list = self.list() + key = sorted(key_list, key=lambda key: key["created_at"])[-1] + key.entity_id = quote_plus(key["value"]) + return key class Methods(DefaultClientNestedCRD, threescale_api.resources.Methods): @@ -1777,7 +1913,7 @@ def in_create(self, maps, params, spec): self.topmost_parent().read() self.topmost_parent().update({"methods": maps}) for mapi in self.get_list(): - if all([params[key] == mapi[key] for key in params.keys()]): + if all(params[key] == mapi[key] for key in params.keys()): return mapi return None @@ -1966,7 +2102,7 @@ def __init__(self, **kwargs): "auth_user_key", "auth_app_id", "auth_app_key", - "api_test_path" + "api_test_path", ] if any([att not in entity for att in required_attrs]): self.client.disable_crd_implemented() @@ -1991,8 +2127,7 @@ def deploy(self): prom.delete() if ide and int(ide) == self.parent["id"]: return True - else: - return False + return False def promote(self, **kwargs): """ @@ -2012,8 +2147,7 @@ def promote(self, **kwargs): prom.delete() if ide and int(ide) == self.parent["id"]: return True - else: - return False + return False @property def service(self) -> "Service": @@ -2150,7 +2284,7 @@ def load_openapi(entity, spec): if "url" in spec: url = spec["url"] entity["url"] = url - res = requests.get(url) + res = requests.get(urli, timeout=60) if url.endswith(".yaml") or url.endswith(".yml"): entity["body"] = json.dumps( yaml.load(res.content, Loader=yaml.SafeLoader) @@ -2940,6 +3074,30 @@ def __init__(self, entity_name="name", **kwargs): super().__init__(crd=crd, entity=entity, entity_name=entity_name, **kwargs) +class AppAuth(DefaultResourceCRD): + """ + CRD resource for ApplicationAuth. + """ + + GET_PATH = "spec" + + def __init__(self, entity_name="", **kwargs): + entity = None + crd = None + if "spec" in kwargs: + spec = kwargs.pop("spec") + crd = kwargs.pop("crd") + + entity = {} + for key, value in spec.items(): + for cey, walue in constants.KEYS_APP_AUTH.items(): + if key == walue: + entity[cey] = value + entity["id"] = entity["authSecretRef"]["name"] + + super().__init__(crd=crd, entity=entity, entity_name=entity_name, **kwargs) + + class Application(DefaultResourceCRD, threescale_api.resources.Application): """ CRD resource for Application. @@ -2994,8 +3152,7 @@ def service(self) -> "Service": def account(self) -> "Account": if self.client.account: return self.client.account - else: - return self.parent.accounts.read_by_name(self.entity["account_name"]) + return self.parent.accounts.read_by_name(self.entity["account_name"]) def set_state(self, state: str): """Sets the state for the resource @@ -3014,14 +3171,23 @@ def set_state(self, state: str): app = self.update({"suspend": False}) status = "live" counters = [89, 55, 34, 21, 13, 8, 5, 3, 2, 1, 1, 1] - while app["state"] != status and len(counters): + while app["state"] != status and counters: time.sleep(counters.pop()) app = app.read() return app + @property + def keys(self): + "Application keys" + return ApplicationKeys( + parent=self, instance_klass=threescale_api.resources.ApplicationKey + ) + class Method(DefaultResourceCRD, threescale_api.resources.Method): + """Method class""" + GET_PATH = "spec/methods" system_name_to_id = {} id_to_system_name = {} @@ -3046,10 +3212,10 @@ def __init__(self, entity_name="system_name", **kwargs): self.entity_id = entity["id"] super().__init__(crd=crd, entity=entity, entity_name=entity_name, **kwargs) - else: - # this is not here because of some backup, but because we need to have option - # to creater empty object without any data. This is related to "lazy load" - super().__init__(entity_name=entity_name, **kwargs) + + # this is not here because of some backup, but because we need to have option + # to creater empty object without any data. This is related to "lazy load" + super().__init__(entity_name=entity_name, **kwargs) @property def metric(self) -> "Metric":