diff --git a/.travis.yml b/.travis.yml index 6dd0c1d..f4dc5d7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ notifications: matrix: include: - - python: 3.5 - python: 3.6 - python: 3.7 - python: 3.8 diff --git a/README.md b/README.md index 6014463..3d2fa80 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Service Name | Imported Class Name * An [IBM Cloud][ibm-cloud-onboarding] account. * An IAM API key to allow the SDK to access your account. Create one [here](https://cloud.ibm.com/iam/apikeys). -* Python 3.5.3 or above. +* Python 3.6 or above. ## Installation diff --git a/example/README.md b/example/README.md index 121b663..5c366b9 100644 --- a/example/README.md +++ b/example/README.md @@ -3,11 +3,13 @@ ## Running example.py To run the example, create a Code Engine project from the Console or Code Engine CLI, and run the following commands from this directory: -1. `pip3 install kubernetes` +1. `pip install kubernetes` 2. `export CE_API_KEY=` 3. `export CE_PROJECT_ID=` 4. `export CE_PROJECT_REGION=` -5. `python3 example.py` +5. `python example.py` + +Note: Requires Python 3.6 or later. ## How-to @@ -28,11 +30,28 @@ ce_client.set_service_url( ) ``` +### Use an HTTP library to get a Delegated Refresh Token from IAM +```python +iam_response = requests.post('https://iam.cloud.ibm.com/identity/token', headers={ + 'Content-Type': 'application/x-www-form-urlencoded' +}, data={ + 'grant_type': 'urn:ibm:params:oauth:grant-type:apikey', + 'apikey': os.environ.get('CE_API_KEY'), + 'response_type': 'delegated_refresh_token', + 'receiver_client_ids': 'ce', + 'delegated_refresh_token_expiry': '3600' +}) +delegated_refresh_token = iam_response.json()['delegated_refresh_token'] +``` + ### Use the Code Engine client to get a Kubernetes config ```python -refresh_token = authenticator.token_manager.request_token().get('refresh_token') -kubeconfig_response = ce_client.list_kubeconfig( - refresh_token=refresh_token, +kubeconfig_response = ce_client.get_kubeconfig( + x_delegated_refresh_token=delegated_refresh_token, id=os.environ.get('CE_PROJECT_ID'), ) +kubeconfig_string = kubeconfig_response.get_result().content ``` + +## Deprecated endpoint +The `/namespaces/{id}/config` endpoint function, `list_kubeconfig()`, is deprecated, and will be removed before Code Engine is out of Beta. Please use the `get_kubeconfig()` function, demonstrated in the example above. diff --git a/example/example.py b/example/example.py index f87bc64..8e5bd52 100644 --- a/example/example.py +++ b/example/example.py @@ -5,6 +5,8 @@ import os import tempfile import kubernetes +import requests +import json from ibm_code_engine_sdk.ibm_cloud_code_engine_v1 import IbmCloudCodeEngineV1 from ibm_cloud_sdk_core.authenticators import IAMAuthenticator @@ -29,18 +31,28 @@ 'https://api.' + os.environ.get('CE_PROJECT_REGION') + '.codeengine.cloud.ibm.com/api/v1' ) -# Get IAM tokens using the authenticator -refresh_token = authenticator.token_manager.request_token().get('refresh_token') +# Get a Delegated Refresh Token from IAM +iam_response = requests.post('https://iam.cloud.ibm.com/identity/token', headers={ + 'Content-Type': 'application/x-www-form-urlencoded' +}, data={ + 'grant_type': 'urn:ibm:params:oauth:grant-type:apikey', + 'apikey': os.environ.get('CE_API_KEY'), + 'response_type': 'delegated_refresh_token', + 'receiver_client_ids': 'ce', + 'delegated_refresh_token_expiry': '3600' +}) +delegated_refresh_token = iam_response.json()['delegated_refresh_token'] # Get Code Engine project config using the Code Engine client. -kubeconfig_response = ce_client.list_kubeconfig( - refresh_token=refresh_token, +kubeconfig_response = ce_client.get_kubeconfig( + x_delegated_refresh_token=delegated_refresh_token, id=os.environ.get('CE_PROJECT_ID'), ) +kubeconfig_string = kubeconfig_response.get_result().content # Setup Kubernetes client using project config kubeconfig_file, kubeconfig_filename = tempfile.mkstemp() -os.write(kubeconfig_file, kubeconfig_response.get_result().content) +os.write(kubeconfig_file, kubeconfig_string) kubernetes.config.load_kube_config(config_file=kubeconfig_filename) kube_client = kubernetes.client.CoreV1Api() diff --git a/example/example_deprecated.py b/example/example_deprecated.py new file mode 100644 index 0000000..1921325 --- /dev/null +++ b/example/example_deprecated.py @@ -0,0 +1,55 @@ +""" +Example of IBM Cloud Code Engine SDK usage +""" + +import os +import tempfile +import kubernetes +from ibm_code_engine_sdk.ibm_cloud_code_engine_v1 import IbmCloudCodeEngineV1 +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator + +if (os.environ.get('CE_API_KEY') == None or + os.environ.get('CE_PROJECT_REGION') == None or + os.environ.get('CE_PROJECT_ID') == None): + print( + 'You must set the envrionment variables CE_API_KEY, CE_PROJECT_REGION and CE_PROJECT_ID ' + + 'before using the example.' + ) + +# Create an IAM authenticator. +authenticator = IAMAuthenticator( + apikey=os.environ.get('CE_API_KEY'), + client_id='bx', + client_secret='bx', +) + +# Construct the Code Engine client. +ce_client = IbmCloudCodeEngineV1(authenticator=authenticator) +ce_client.set_service_url( + 'https://api.' + os.environ.get('CE_PROJECT_REGION') + '.codeengine.cloud.ibm.com/api/v1' +) + +# Get IAM tokens using the authenticator +refresh_token = authenticator.token_manager.request_token().get('refresh_token') + +# Get Code Engine project config using the Code Engine client. +kubeconfig_response = ce_client.list_kubeconfig( + refresh_token=refresh_token, + id=os.environ.get('CE_PROJECT_ID'), +) +kubeconfig_string = kubeconfig_response.get_result().content + +# Setup Kubernetes client using project config +kubeconfig_file, kubeconfig_filename = tempfile.mkstemp() +os.write(kubeconfig_file, kubeconfig_string) +kubernetes.config.load_kube_config(config_file=kubeconfig_filename) +kube_client = kubernetes.client.CoreV1Api() + +# Get something from project. +contexts = kubernetes.config.list_kube_config_contexts(config_file=kubeconfig_filename)[0][0] +namespace = contexts.get('context').get('namespace') +configmaps = kube_client.list_namespaced_config_map(namespace) +print( + 'Project ' + os.environ.get('CE_PROJECT_ID') + + ' has ' + str(len(configmaps.items)) + ' configmaps.' +) diff --git a/ibm_code_engine_sdk/ibm_cloud_code_engine_v1.py b/ibm_code_engine_sdk/ibm_cloud_code_engine_v1.py index a5f23d2..27b9d62 100644 --- a/ibm_code_engine_sdk/ibm_cloud_code_engine_v1.py +++ b/ibm_code_engine_sdk/ibm_cloud_code_engine_v1.py @@ -1,6 +1,6 @@ # coding: utf-8 -# (C) Copyright IBM Corp. 2020. +# (C) Copyright IBM Corp. 2021. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -# IBM OpenAPI SDK Code Generator Version: 3.12.0-64fe8d3f-20200820-144050 +# IBM OpenAPI SDK Code Generator Version: 3.15.0-45841b53-20201019-214802 """ -The purpose is to provide an API to get Kubeconfig for IBM Cloud Code Engine Project +The purpose is to provide an API to get Kubeconfig file for IBM Cloud Code Engine Project """ from enum import Enum @@ -81,20 +81,22 @@ def list_kubeconfig(self, **kwargs ) -> DetailedResponse: """ - Retrieve KUBECONFIG for a specified project. + Deprecated soon: Retrieve KUBECONFIG for a specified project. - Returns the KUBECONFIG, similar to the output of `kubectl config view - --minify=true`. + **Deprecated soon**: This API will be deprecated soon. Use the [GET + /project/{id}/config](#get-kubeconfig) API instead. Returns the KUBECONFIG file, + similar to the output of `kubectl config view --minify=true`. :param str refresh_token: The IAM Refresh token associated with the IBM - Cloud account. - :param str id: The id of the IBM Cloud Code Engine project. - :param str accept: (optional) The type of the response: application/json or - text/html. A character encoding can be specified by including a `charset` - parameter. For example, 'text/html;charset=utf-8'. + Cloud account. To retrieve your IAM token, run `ibmcloud iam oauth-tokens`. + :param str id: The id of the IBM Cloud Code Engine project. To retrieve + your project ID, run `ibmcloud ce project get -n `. + :param str accept: (optional) The type of the response: text/plain or + application/json. A character encoding can be specified by including a + `charset` parameter. For example, 'text/plain;charset=utf-8'. :param dict headers: A `dict` containing the request headers :return: A `DetailedResponse` containing the result, headers and HTTP status code. - :rtype: DetailedResponse + :rtype: DetailedResponse with `str` result """ if refresh_token is None: @@ -113,8 +115,82 @@ def list_kubeconfig(self, if 'headers' in kwargs: headers.update(kwargs.get('headers')) - url = '/namespaces/{0}/config'.format( - *self.encode_path_vars(id)) + path_param_keys = ['id'] + path_param_values = self.encode_path_vars(id) + path_param_dict = dict(zip(path_param_keys, path_param_values)) + url = '/namespaces/{id}/config'.format(**path_param_dict) + request = self.prepare_request(method='GET', + url=url, + headers=headers) + + response = self.send(request) + return response + + + def get_kubeconfig(self, + x_delegated_refresh_token: str, + id: str, + *, + accept: str = None, + **kwargs + ) -> DetailedResponse: + """ + Retrieve KUBECONFIG for a specified project. + + Returns the KUBECONFIG, similar to the output of `kubectl config view + --minify=true`. There are 2 tokens in the Request Header and a query parameter + that you must provide. + These values can be generated as follows: 1. Auth Header Pass the generated IAM + Token as the Authorization header from the CLI as `token=cat + $HOME/.bluemix/config.json | jq .IAMToken -r`. Generate the token with the [Create + an IAM access token for a user or service ID using an API + key](https://cloud.ibm.com/apidocs/iam-identity-token-api#gettoken-apikey) API. + 2. X-Delegated-Refresh-Token Header Generate an IAM Delegated Refresh Token for + Code Engine with the [Create an IAM access token and delegated refresh token for a + user or service + ID](https://cloud.ibm.com/apidocs/iam-identity-token-api#gettoken-apikey-delegatedrefreshtoken) + API. Specify the `receiver_client_ids` value to be `ce` and the + `delegated_refresh_token_expiry` value to be `3600`. + 3. Project ID In order to retrieve the Kubeconfig file for a specific Code Engine + project, use the CLI to extract the ID + `id=ibmcloud ce project get -n ${CE_PROJECT_NAME} -o jsonpath={.guid}` You must be + logged into the account where the project was created to retrieve the ID. + + :param str x_delegated_refresh_token: This IAM Delegated Refresh Token is + specifically valid for Code Engine. Generate this token with the [Create an + IAM access token and delegated refresh token for a user or service + ID](https://cloud.ibm.com/apidocs/iam-identity-token-api#gettoken-apikey-delegatedrefreshtoken) + API. Specify the `receiver_client_ids` value to be `ce` and the + `delegated_refresh_token_expiry` value to be `3600`. + :param str id: The id of the IBM Cloud Code Engine project. + :param str accept: (optional) The type of the response: text/plain or + application/json. A character encoding can be specified by including a + `charset` parameter. For example, 'text/plain;charset=utf-8'. + :param dict headers: A `dict` containing the request headers + :return: A `DetailedResponse` containing the result, headers and HTTP status code. + :rtype: DetailedResponse with `str` result + """ + + if x_delegated_refresh_token is None: + raise ValueError('x_delegated_refresh_token must be provided') + if id is None: + raise ValueError('id must be provided') + headers = { + 'X-Delegated-Refresh-Token': x_delegated_refresh_token, + 'Accept': accept + } + sdk_headers = get_sdk_headers(service_name=self.DEFAULT_SERVICE_NAME, + service_version='V1', + operation_id='get_kubeconfig') + headers.update(sdk_headers) + + if 'headers' in kwargs: + headers.update(kwargs.get('headers')) + + path_param_keys = ['id'] + path_param_values = self.encode_path_vars(id) + path_param_dict = dict(zip(path_param_keys, path_param_values)) + url = '/project/{id}/config'.format(**path_param_dict) request = self.prepare_request(method='GET', url=url, headers=headers) @@ -130,12 +206,27 @@ class ListKubeconfigEnums: class Accept(str, Enum): """ - The type of the response: application/json or text/html. A character encoding can + The type of the response: text/plain or application/json. A character encoding can + be specified by including a `charset` parameter. For example, + 'text/plain;charset=utf-8'. + """ + TEXT_PLAIN = 'text/plain' + APPLICATION_JSON = 'application/json' + + +class GetKubeconfigEnums: + """ + Enums for get_kubeconfig parameters. + """ + + class Accept(str, Enum): + """ + The type of the response: text/plain or application/json. A character encoding can be specified by including a `charset` parameter. For example, - 'text/html;charset=utf-8'. + 'text/plain;charset=utf-8'. """ + TEXT_PLAIN = 'text/plain' APPLICATION_JSON = 'application/json' - TEXT_HTML = 'text/html' ############################################################################## diff --git a/test/unit/test_ibm_cloud_code_engine_v1.py b/test/unit/test_ibm_cloud_code_engine_v1.py index 7222c07..95955ae 100644 --- a/test/unit/test_ibm_cloud_code_engine_v1.py +++ b/test/unit/test_ibm_cloud_code_engine_v1.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# (C) Copyright IBM Corp. 2020. +# (C) Copyright IBM Corp. 2021. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,7 +48,6 @@ def preprocess_url(self, request_url: str): """ Preprocess the request URL to ensure the mock response will be found. """ - request_url = urllib.parse.quote(request_url, safe=':/') if re.fullmatch('.*/+', request_url) is None: return request_url else: @@ -61,14 +60,17 @@ def test_list_kubeconfig_all_params(self): """ # Set up mock url = self.preprocess_url(base_url + '/namespaces/testString/config') + mock_response = '"operation_response"' responses.add(responses.GET, url, + body=mock_response, + content_type='text/plain', status=200) # Set up parameter values refresh_token = 'testString' id = 'testString' - accept = 'application/json' + accept = 'text/plain' # Invoke method response = service.list_kubeconfig( @@ -90,8 +92,11 @@ def test_list_kubeconfig_required_params(self): """ # Set up mock url = self.preprocess_url(base_url + '/namespaces/testString/config') + mock_response = '"operation_response"' responses.add(responses.GET, url, + body=mock_response, + content_type='text/plain', status=200) # Set up parameter values @@ -117,8 +122,11 @@ def test_list_kubeconfig_value_error(self): """ # Set up mock url = self.preprocess_url(base_url + '/namespaces/testString/config') + mock_response = '"operation_response"' responses.add(responses.GET, url, + body=mock_response, + content_type='text/plain', status=200) # Set up parameter values @@ -137,6 +145,112 @@ def test_list_kubeconfig_value_error(self): +class TestGetKubeconfig(): + """ + Test Class for get_kubeconfig + """ + + def preprocess_url(self, request_url: str): + """ + Preprocess the request URL to ensure the mock response will be found. + """ + if re.fullmatch('.*/+', request_url) is None: + return request_url + else: + return re.compile(request_url.rstrip('/') + '/+') + + @responses.activate + def test_get_kubeconfig_all_params(self): + """ + get_kubeconfig() + """ + # Set up mock + url = self.preprocess_url(base_url + '/project/testString/config') + mock_response = '"operation_response"' + responses.add(responses.GET, + url, + body=mock_response, + content_type='text/plain', + status=200) + + # Set up parameter values + x_delegated_refresh_token = 'testString' + id = 'testString' + accept = 'text/plain' + + # Invoke method + response = service.get_kubeconfig( + x_delegated_refresh_token, + id, + accept=accept, + headers={} + ) + + # Check for correct operation + assert len(responses.calls) == 1 + assert response.status_code == 200 + + + @responses.activate + def test_get_kubeconfig_required_params(self): + """ + test_get_kubeconfig_required_params() + """ + # Set up mock + url = self.preprocess_url(base_url + '/project/testString/config') + mock_response = '"operation_response"' + responses.add(responses.GET, + url, + body=mock_response, + content_type='text/plain', + status=200) + + # Set up parameter values + x_delegated_refresh_token = 'testString' + id = 'testString' + + # Invoke method + response = service.get_kubeconfig( + x_delegated_refresh_token, + id, + headers={} + ) + + # Check for correct operation + assert len(responses.calls) == 1 + assert response.status_code == 200 + + + @responses.activate + def test_get_kubeconfig_value_error(self): + """ + test_get_kubeconfig_value_error() + """ + # Set up mock + url = self.preprocess_url(base_url + '/project/testString/config') + mock_response = '"operation_response"' + responses.add(responses.GET, + url, + body=mock_response, + content_type='text/plain', + status=200) + + # Set up parameter values + x_delegated_refresh_token = 'testString' + id = 'testString' + + # Pass in all but one required param and check for a ValueError + req_param_dict = { + "x_delegated_refresh_token": x_delegated_refresh_token, + "id": id, + } + for param in req_param_dict.keys(): + req_copy = {key:val if key is not param else None for (key,val) in req_param_dict.items()} + with pytest.raises(ValueError): + service.get_kubeconfig(**req_copy) + + + # endregion ############################################################################## # End of Service: GetKubeconfig