Skip to content

Commit d411cd9

Browse files
committed
updated logic
1 parent 90f1b12 commit d411cd9

5 files changed

Lines changed: 146 additions & 107 deletions

File tree

CHANGES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@
3939
- Updated keyId of an API KEY to be the actual ID and not the key itself
4040
3.2.0 (Feb 2, 2025)
4141
- Updated to support flag sets, large segments and the impressionsDisabled boolean value
42-
3.5.0 (Mar 14, 2025)
42+
3.5.0 (May 6, 2025)
4343
- Updated to support harness mode

README.md

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

23-
### Harness Mode
23+
## Using in Harness Mode
2424

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.
25+
Starting with version 3.5.0, the Split API client supports operating in "harness mode" to interact with both Split and Harness Feature Flags APIs.
2626

27-
#### Authentication
27+
For detailed information about Harness Feature Flags API endpoints, please refer to the [official Harness API documentation](https://apidocs.harness.io/).
2828

29-
In harness mode, authentication can be configured in two ways:
30-
- Use `harness_token` for Harness endpoints and `apikey` for Split endpoints
31-
- If `harness_token` is not provided, `apikey` will be used for all operations
29+
### Authentication in Harness Mode
3230

33-
Harness endpoints use the 'x-api-key' header instead of the standard Split authentication.
31+
The client supports multiple authentication scenarios:
3432

35-
#### Base URLs and Endpoints
33+
1. Harness-specific endpoints always use the 'x-api-key' header format
34+
2. Split endpoints will use the 'x-api-key' header when using the harness_token
35+
3. Split endpoints will use the normal 'Authorization' header when using the apikey
36+
4. If both harness_token and apikey are provided, the client will use the harness_token for Harness endpoints and the apikey for Split endpoints
37+
38+
### Base URLs and Endpoints
3639

3740
- Existing, non-deprecated Split endpoints continue to use the Split base URLs
3841
- New Harness-specific endpoints use the Harness base URL (https://app.harness.io/)
39-
- There is no v3 version for the Harness API
4042

41-
#### Deprecated Endpoints
43+
### Deprecated Endpoints
4244

4345
The following endpoints are deprecated and cannot be used in harness mode:
4446
- `/workspaces`: POST, PATCH, DELETE, PUT verbs are deprecated
@@ -47,7 +49,7 @@ The following endpoints are deprecated and cannot be used in harness mode:
4749
- `/groups`: all verbs are deprecated
4850
- `/restrictions`: all verbs are deprecated
4951

50-
#### Basic Usage
52+
### Basic Usage
5153

5254
To use the client in harness mode:
5355

@@ -58,17 +60,20 @@ from splitapiclient.main import get_client
5860
client = get_client({
5961
'harness_mode': True,
6062
'harness_token': 'YOUR_HARNESS_TOKEN', # Used for Harness-specific endpoints
61-
'apikey': 'YOUR_SPLIT_API_KEY' # Used for existing Split endpoints
63+
'apikey': 'YOUR_SPLIT_API_KEY', # Used for existing Split endpoints
64+
'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID' # Required for Harness operations
6265
})
6366

64-
# Option 2: Use apikey for all operations (if harness_token is not provided)
67+
# Option 2: Use harness_token for all operations (if apikey is not provided)
6568
client = get_client({
6669
'harness_mode': True,
67-
'apikey': 'YOUR_API_KEY' # Used for both Split and Harness endpoints
70+
'harness_token': 'YOUR_HARNESS_TOKEN', # Used for both Harness and Split endpoints
71+
'account_identifier': 'YOUR_HARNESS_ACCOUNT_ID'
6872
})
73+
6974
```
7075

71-
#### Working with Split Resources in Harness Mode
76+
### Working with Split Resources in Harness Mode
7277

7378
You can still access standard Split resources with some restrictions:
7479

@@ -83,113 +88,58 @@ ws = client.workspaces.find("Default")
8388
# List environments in a workspace
8489
for env in client.environments.list(ws.id):
8590
print(f"Environment: {env.name}, Id: {env.id}")
86-
87-
# List splits in a workspace
88-
for split in client.splits.list(ws.id):
89-
print(f"Split: {split.name}")
9091
```
9192

92-
#### Working with Harness-specific Resources
93+
### Working with Harness-specific Resources
94+
95+
Harness mode provides access to several Harness-specific resources through dedicated microclients:
96+
97+
- token
98+
- harness_apikey
99+
- service_account
100+
- harness_user
101+
- harness_group
102+
- role
103+
- resource_group
104+
- role_assignment
105+
- harness_project
93106

94-
Harness mode provides access to several Harness-specific resources:
107+
Basic example:
95108

96109
```python
97-
# Account identifier is required for Harness operations
110+
# Account identifier is required for all Harness operations
98111
account_id = 'YOUR_ACCOUNT_IDENTIFIER'
99112

100-
# List tokens
101-
for token in client.token.list(account_id):
113+
# List all tokens
114+
tokens = client.token.list(account_id)
115+
for token in tokens:
102116
print(f"Token: {token.name}, ID: {token.id}")
103117

104-
# Get a specific token
105-
token = client.token.get('TOKEN_ID', account_id)
106-
107-
# List Harness API keys
108-
for api_key in client.harness_apikey.list(api_key_type="STANDARD", account_identifier=account_id):
109-
print(f"API Key: {api_key.name}, ID: {api_key.id}")
110-
111118
# List service accounts
112-
for sa in client.service_account.list(account_id):
119+
service_accounts = client.service_account.list(account_id)
120+
for sa in service_accounts:
113121
print(f"Service Account: {sa.name}, ID: {sa.id}")
114-
115-
# List Harness users
116-
for user in client.harness_user.list(account_id):
117-
print(f"User: {user.name}, Email: {user.email}")
118-
119-
# List Harness groups
120-
for group in client.harness_group.list(account_id):
121-
print(f"Group: {group.name}, ID: {group.id}")
122-
123-
# List roles
124-
for role in client.role.list(account_id):
125-
print(f"Role: {role.name}, ID: {role.id}")
126-
127-
# List resource groups
128-
for rg in client.resource_group.list(account_id):
129-
print(f"Resource Group: {rg.name}, ID: {rg.id}")
130-
131-
# List role assignments
132-
for ra in client.role_assignment.list(account_id):
133-
print(f"Role Assignment: {ra.id}, Role: {ra.role_identifier}")
134-
135-
# List Harness projects
136-
for project in client.harness_project.list(account_identifier=account_id):
137-
print(f"Project: {project.name}, ID: {project.identifier}")
138-
139-
# Get a specific project
140-
project = client.harness_project.get('PROJECT_ID', account_identifier=account_id)
141122
```
142123

143-
#### Account Identifier
124+
For detailed examples and API specifications for each resource, please refer to the [Harness API documentation](https://apidocs.harness.io/).
144125

145-
The `account_identifier` is a required parameter for all Harness operations. You have two options for providing it:
126+
### Setting Default Account Identifier
146127

147-
1. Specify it during client initialization (recommended):
128+
To avoid specifying the account identifier with every request:
148129

149130
```python
150-
from splitapiclient.main import get_client
151-
152-
# Specify account_identifier during initialization
131+
# Set default account identifier when creating the client
153132
client = get_client({
154133
'harness_mode': True,
155-
'harness_token': 'YOUR_HARNESS_TOKEN',
156-
'apikey': 'YOUR_SPLIT_API_KEY',
157-
'account_identifier': 'YOUR_ACCOUNT_ID' # Will be used for all Harness operations
134+
'harness_token': 'YOUR_HARNESS_TOKEN',
135+
'account_identifier': 'YOUR_ACCOUNT_IDENTIFIER'
158136
})
159137

160138
# Now you can make calls without specifying account_identifier in each request
161139
tokens = client.token.list() # account_identifier is automatically included
162140
projects = client.harness_project.list() # account_identifier is automatically included
163141
```
164142

165-
2. Provide it with each API call:
166-
167-
```python
168-
from splitapiclient.main import get_client
169-
170-
# Initialize without account_identifier
171-
client = get_client({
172-
'harness_mode': True,
173-
'harness_token': 'YOUR_HARNESS_TOKEN',
174-
'apikey': 'YOUR_SPLIT_API_KEY'
175-
})
176-
177-
# Must specify account_identifier with each API call
178-
tokens = client.token.list(account_identifier='YOUR_ACCOUNT_ID')
179-
projects = client.harness_project.list(account_identifier='YOUR_ACCOUNT_ID')
180-
```
181-
182-
If you don't provide the account_identifier either during initialization or with each API call, the client will raise a ValueError when you try to access Harness resources.
183-
184-
You can still override the account_identifier for specific calls even if you provided it during initialization:
185-
186-
```python
187-
# Override the default account_identifier for a specific call
188-
other_account_projects = client.harness_project.list(account_identifier='DIFFERENT_ACCOUNT_ID')
189-
```
190-
191-
The account identifier can be found in your Harness account settings or through the Harness UI. It is typically in the format of a unique string identifying your organization in the Harness platform.
192-
193143
### Testing API Endpoints
194144

195145
#### Example: Creating and Managing Split Resources

splitapiclient/http_clients/harness_client.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from splitapiclient.util.logger import LOGGER
99
from splitapiclient.util.exceptions import HTTPResponseError, \
1010
HTTPNotFoundError, HTTPIncorrectParametersError, HTTPUnauthorizedError, \
11-
SplitBackendUnreachableError, HarnessDeprecatedEndpointError
11+
SplitBackendUnreachableError, HarnessDeprecatedEndpointError, MissingParametersException
1212

1313

1414
class HarnessHttpClient(base_client.BaseHttpClient):
@@ -36,11 +36,13 @@ def __init__(self, baseurl, auth_token):
3636
:param baseurl: string. Harness host and base url.
3737
:param auth_token: string. Harness authentication token needed to make API calls.
3838
'''
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
39+
# Initialize with empty base_args - we'll handle auth differently in harness mode
40+
self.config = {
41+
'base_url': baseurl,
42+
'base_args': {}
4343
}
44+
# Store the auth token
45+
self._auth_token = auth_token
4446

4547
def setup_method(self, method, body=None):
4648
'''
@@ -97,6 +99,18 @@ def _is_deprecated_endpoint(self, endpoint, body=None):
9799

98100
return False
99101

102+
def _is_harness_endpoint(self, endpoint):
103+
'''
104+
Determines if the endpoint is a Harness-specific endpoint based on the base URL.
105+
106+
:param endpoint: dict. Endpoint description.
107+
:return: bool. True if the endpoint is a Harness endpoint, False otherwise.
108+
'''
109+
# In the harness client, we can determine if it's a Harness endpoint by checking the base URL
110+
# Split API base URLs are typically api.split.io
111+
# Harness API base URLs are typically app.harness.io
112+
return 'app.harness.io' in self.config['base_url']
113+
100114
def _handle_invalid_response(self, response):
101115
'''
102116
Handle responses that are not okay and throw an appropriate exception.
@@ -128,6 +142,77 @@ def _handle_connection_error(self, e):
128142
raise SplitBackendUnreachableError(
129143
'Unable to reach Harness backend'
130144
)
145+
146+
@staticmethod
147+
def validate_params(endpoint, all_arguments):
148+
'''
149+
Override the base client validation to handle harness mode authentication.
150+
In harness mode, we use x-api-key instead of Authorization, so we need to
151+
modify the validation logic to not require the Authorization header.
152+
153+
:param endpoint: dict. Endpoint description
154+
:param all_arguments: Parameter values
155+
156+
:rtype: None
157+
'''
158+
# Get required parameters from URL template and query string
159+
url_params = base_client.BaseHttpClient.get_params_from_url_template(endpoint['url_template'])
160+
query_params = [i['name'] for i in endpoint['query_string'] if i['required']]
161+
162+
# Add required headers, but exclude 'Authorization' since we're using 'x-api-key' in harness mode
163+
header_params = []
164+
for header in endpoint['headers']:
165+
if header['required'] and header['name'] != 'Authorization':
166+
header_params.append(header['name'])
167+
168+
# Combine all required parameters
169+
required_params = url_params + header_params + query_params
170+
171+
# Check if any required parameters are missing
172+
missing = [p for p in required_params if p not in all_arguments]
173+
174+
if missing:
175+
raise MissingParametersException(
176+
'The following required parameters are missing: {missing}'
177+
.format(missing=', '.join(missing))
178+
)
179+
180+
def _setup_headers(self, endpoint, params):
181+
'''
182+
Override the base client _setup_headers method to handle harness mode authentication.
183+
In harness mode, we need to skip 'Authorization' headers and use 'x-api-key' instead.
184+
185+
:param endpoint: dict. Endpoint description
186+
:param params: dict. List of parameter values
187+
188+
:rtype: dict.
189+
'''
190+
# Set up required headers except 'Authorization'
191+
headers = {}
192+
for header in endpoint['headers']:
193+
if header.get('required', False):
194+
# Skip 'Authorization' header in harness mode
195+
if header['name'] == 'Authorization':
196+
continue
197+
if header['name'] in params:
198+
headers[header['name']] = base_client.BaseHttpClient._process_single_header(
199+
header, params[header['name']]
200+
)
201+
202+
# Add optional headers
203+
headers.update({
204+
header['name']: base_client.BaseHttpClient._process_single_header(
205+
header, params[header['name']]
206+
)
207+
for header in endpoint['headers']
208+
if (not header.get('required', False)) and header['name'] in params
209+
})
210+
211+
# Add x-api-key header
212+
if 'x-api-key' in params:
213+
headers['x-api-key'] = params['x-api-key']
214+
215+
return headers
131216

132217
def make_request(self, endpoint, body=None, **kwargs):
133218
'''
@@ -148,6 +233,9 @@ def make_request(self, endpoint, body=None, **kwargs):
148233
f"Endpoint {endpoint['url_template']} with method {endpoint['method']} is deprecated in harness mode"
149234
)
150235

236+
# In harness mode, use x-api-key header for all endpoints
237+
kwargs['x-api-key'] = self._auth_token
238+
151239
kwargs.update(self.config['base_args'])
152240
self.validate_params(endpoint, kwargs)
153241

splitapiclient/main/harness_apiclient.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ def __init__(self, config):
8282
self._apikey = config.get('apikey')
8383
self._harness_token = config.get('harness_token')
8484

85-
# Store the account identifier
86-
self._account_identifier = config.get('account_identifier')
87-
8885
# If harness_token is not provided, use apikey for all operations
89-
split_auth_token = self._apikey
86+
# If apikey is not provided, use harness_token for all operations
87+
split_auth_token = self._apikey if self._apikey else self._harness_token
9088
harness_auth_token = self._harness_token if self._harness_token else self._apikey
9189

90+
# Store the account identifier
91+
self._account_identifier = config.get('account_identifier')
92+
9293
# Create HTTP clients for Split endpoints
9394
split_http_client = HarnessHttpClient(self._base_url, split_auth_token)
9495
split_http_clientv3 = HarnessHttpClient(self._base_url_v3, split_auth_token)

splitapiclient/microclients/harness/service_account_microclient.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class ServiceAccountMicroClient:
2525
},
2626
'item': {
2727
'method': 'GET',
28-
'url_template': '/ng/api/serviceaccount/{serviceAccountId}?accountIdentifier={accountIdentifier}',
28+
'url_template': '/ng/api/serviceaccount/aggregate/{serviceAccountId}?accountIdentifier={accountIdentifier}',
2929
'headers': [{
3030
'name': 'x-api-key',
3131
'template': '{value}',

0 commit comments

Comments
 (0)