Skip to content

Commit d5e71aa

Browse files
committed
initial commit
1 parent 7ef691f commit d5e71aa

39 files changed

Lines changed: 7901 additions & 0 deletions

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,64 @@ from splitapiclient.main import get_client
2020
client = get_client({'apikey': 'ADMIN API KEY'})
2121
```
2222

23+
### Harness Mode
24+
25+
Split has been acquired by Harness. This client now supports a 'harness_mode' which uses a different authentication mechanism and provides access to Harness-specific resources.
26+
27+
In harness mode:
28+
- Existing, non-deprecated Split endpoints continue to use the Split base URLs
29+
- New Harness-specific endpoints use the Harness base URL
30+
- Authentication can be configured in two ways:
31+
- Use `harness_token` for Harness endpoints and `apikey` for Split endpoints
32+
- If `harness_token` is not provided, `apikey` will be used for all operations
33+
34+
The following endpoints are deprecated and cannot be used in harness mode:
35+
- `/workspaces`: POST, PATCH, DELETE verbs
36+
- `/apiKeys`: POST for apiKeyType == 'admin'
37+
- `/users`: all verbs
38+
- `/groups`: all verbs
39+
- `/restrictions`: all verbs
40+
41+
To use the client in harness mode:
42+
43+
```python
44+
from splitapiclient.main import get_client
45+
46+
# Option 1: Use harness_token for Harness endpoints and apikey for Split endpoints
47+
client = get_client({
48+
'harness_mode': True,
49+
'harness_token': 'YOUR_HARNESS_TOKEN', # Used for Harness-specific endpoints
50+
'apikey': 'YOUR_SPLIT_API_KEY' # Used for existing Split endpoints
51+
})
52+
53+
# Option 2: Use apikey for all operations (if harness_token is not provided)
54+
client = get_client({
55+
'harness_mode': True,
56+
'apikey': 'YOUR_API_KEY' # Used for both Split and Harness endpoints
57+
})
58+
59+
# Access standard Split resources (with restrictions)
60+
for ws in client.workspaces.list():
61+
print(f"Workspace: {ws.name}, Id: {ws.id}")
62+
63+
# Access harness-specific resources
64+
for token in client.token.list():
65+
print(f"Token: {token.name}")
66+
67+
for user in client.harness_user.list():
68+
print(f"User: {user.name}, Email: {user.email}")
69+
```
70+
71+
Harness mode provides the following additional microclients:
72+
- `token`: Manage authentication tokens
73+
- `harness_apikey`: Manage Harness API keys
74+
- `service_account`: Manage service accounts
75+
- `harness_user`: Manage Harness users
76+
- `harness_group`: Manage Harness groups
77+
- `role`: Manage roles
78+
- `resource_group`: Manage resource groups
79+
- `role_assignment`: Manage role assignments
80+
2381

2482
Enable optional logging:
2583

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from __future__ import absolute_import, division, print_function, \
2+
unicode_literals
3+
import json
4+
import time
5+
from functools import partial
6+
import requests
7+
from splitapiclient.http_clients import base_client
8+
from splitapiclient.util.logger import LOGGER
9+
from splitapiclient.util.exceptions import HTTPResponseError, \
10+
HTTPNotFoundError, HTTPIncorrectParametersError, HTTPUnauthorizedError, \
11+
SplitBackendUnreachableError, HarnessDeprecatedEndpointError
12+
13+
14+
class HarnessHttpClient(base_client.BaseHttpClient):
15+
'''
16+
Harness mode HTTP client.
17+
This client will block on every http request until a response is received.
18+
It will also enforce restrictions on deprecated endpoints in harness mode.
19+
'''
20+
21+
# List of deprecated endpoints in harness mode
22+
DEPRECATED_ENDPOINTS = {
23+
'workspaces': ['POST', 'PATCH', 'DELETE', 'PUT'],
24+
'apiKeys': {
25+
'admin': ['POST', 'DELETE']
26+
},
27+
'users': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'],
28+
'groups': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT'],
29+
'restrictions': ['GET', 'POST', 'PATCH', 'DELETE', 'PUT']
30+
}
31+
32+
def __init__(self, baseurl, auth_token):
33+
'''
34+
Class constructor. Stores basic connection information.
35+
36+
:param baseurl: string. Harness host and base url.
37+
:param auth_token: string. Harness authentication token needed to make API calls.
38+
'''
39+
super(HarnessHttpClient, self).__init__(baseurl, auth_token)
40+
# Override the authorization header to use x-api-key
41+
self.config['base_args'] = {
42+
'x-api-key': auth_token
43+
}
44+
45+
def setup_method(self, method, body=None):
46+
'''
47+
Wraps 'requests' module functions by partially applying the body
48+
parameter when needed to provide a standardized interface.
49+
50+
:param method: string. GET | POST | PATCH | PUT | DELETE
51+
:param body: object/list. Body for methods that use it.
52+
53+
:rtype: function
54+
'''
55+
methods = {
56+
'GET': requests.get,
57+
'POST': partial(requests.post, json=body),
58+
'PUT': partial(requests.put, json=body),
59+
'PATCH': partial(requests.patch, json=body),
60+
'DELETE': requests.delete
61+
}
62+
63+
return methods[method]
64+
65+
def _is_deprecated_endpoint(self, endpoint, body=None):
66+
'''
67+
Checks if the endpoint is deprecated in harness mode.
68+
69+
:param endpoint: dict. Endpoint description.
70+
:param body: dict/list. Request body.
71+
72+
:return: bool. True if the endpoint is deprecated, False otherwise.
73+
'''
74+
url_template = endpoint['url_template']
75+
method = endpoint['method']
76+
77+
# Check for workspaces endpoint
78+
if url_template.startswith('workspaces') and method in self.DEPRECATED_ENDPOINTS['workspaces']:
79+
return True
80+
81+
# Check for apiKeys endpoint with admin type
82+
if url_template.startswith('apiKeys') and method in self.DEPRECATED_ENDPOINTS['apiKeys']['admin']:
83+
if body and (body.get('apiKeyType') == 'admin' or method != 'DELETE'):
84+
return True
85+
86+
# Check for users endpoint
87+
if url_template.startswith('users'):
88+
return True
89+
90+
# Check for groups endpoint
91+
if url_template.startswith('groups'):
92+
return True
93+
94+
# Check for restrictions endpoint
95+
if url_template.startswith('restrictions'):
96+
return True
97+
98+
return False
99+
100+
def _handle_invalid_response(self, response):
101+
'''
102+
Handle responses that are not okay and throw an appropriate exception.
103+
If the code doesn't match the known ones, a generic HTTPResponseError
104+
is thrown
105+
106+
:param response: requests' module response object
107+
'''
108+
status_codes_exceptions = {
109+
404: HTTPNotFoundError,
110+
401: HTTPUnauthorizedError,
111+
400: HTTPIncorrectParametersError,
112+
}
113+
114+
exc = status_codes_exceptions.get(response.status_code)
115+
if exc:
116+
raise exc(response.text)
117+
else:
118+
raise HTTPResponseError(response.text)
119+
120+
def _handle_connection_error(self, e):
121+
'''
122+
Handle error when attempting to connect to split backend.
123+
Logs exception thrown by requests module, and raises an
124+
SplitBackendUnreachableError error, so that it can be caught
125+
by using the top level SplitException
126+
'''
127+
LOGGER.debug(e)
128+
raise SplitBackendUnreachableError(
129+
'Unable to reach Harness backend'
130+
)
131+
132+
def make_request(self, endpoint, body=None, **kwargs):
133+
'''
134+
This method delegates building of headers, url and querystring (!)
135+
to separate functions and then calls the appropriate method of the
136+
requests module with the required arguments. Logs plenty of debug data,
137+
and raises an exception if the response code is not 200.
138+
139+
:param endpoint: dict. Endpoint description (url, headers, qs, etc).
140+
:param body: list/dict. Body used for POST/PATCH/PUT requests
141+
:param kwargs: dict. Extra arguments (values).
142+
143+
:rtype: dict/list/None
144+
'''
145+
# Check if the endpoint is deprecated in harness mode
146+
if self._is_deprecated_endpoint(endpoint, body):
147+
raise HarnessDeprecatedEndpointError(
148+
f"Endpoint {endpoint['url_template']} with method {endpoint['method']} is deprecated in harness mode"
149+
)
150+
151+
kwargs.update(self.config['base_args'])
152+
self.validate_params(endpoint, kwargs)
153+
154+
url = self._setup_url(endpoint, kwargs)
155+
headers = self._setup_headers(endpoint, kwargs)
156+
method_name = endpoint['method']
157+
method = self.setup_method(method_name, body)
158+
159+
LOGGER.debug('{method} {url}'.format(method=method_name, url=url))
160+
LOGGER.debug('HEADERS: {headers}'.format(headers=headers))
161+
if body:
162+
LOGGER.debug('BODY: ' + json.dumps(body))
163+
164+
# Make the actual HTTP call!
165+
while True:
166+
try:
167+
response = method(url, headers=headers)
168+
LOGGER.debug('RESPONSE: ' + response.text)
169+
except Exception as e:
170+
return self._handle_connection_error(e)
171+
if response.status_code==429:
172+
LOGGER.warning('RESPONSE CODE: %s, retrying in 5 seconds' % response.status_code)
173+
time.sleep(5)
174+
continue
175+
else:
176+
break
177+
178+
if not (response.status_code == 200 or response.status_code == 204 or response.status_code == 201):
179+
LOGGER.warning('RESPONSE CODE: %s' % response.status_code)
180+
self._handle_invalid_response(response)
181+
182+
if endpoint.get('response', False):
183+
if response.status_code != 204:
184+
return json.loads(response.text)

splitapiclient/main/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
from splitapiclient.main.sync_apiclient import SyncApiClient
2+
from splitapiclient.main.harness_apiclient import HarnessApiClient
23

34

45
def get_client(config):
56
'''
67
Entry point for the Split API client
8+
9+
:param config: Dictionary containing client configuration options
10+
For standard mode:
11+
- 'apikey': Split API key for authentication
12+
- 'base_url': (optional) Base URL for the Split API
13+
- 'base_url_v3': (optional) Base URL for the Split API v3
14+
- 'async': (optional) Whether to use async client (not yet implemented)
15+
16+
For harness mode:
17+
- 'harness_mode': Set to True to use harness mode
18+
- 'harness_token': Harness authentication token for x-api-key header
19+
- 'base_url': (optional) Base URL for the Harness API
20+
- 'base_url_v3': (optional) Base URL for the Harness API v3
721
'''
822
_async = config.get('async', False)
923
if _async:
1024
raise Exception('Async client not yet implemented')
25+
26+
# Check if harness mode is enabled
27+
if config.get('harness_mode', False):
28+
return HarnessApiClient(config)
1129

1230
return SyncApiClient(config)

0 commit comments

Comments
 (0)