Skip to content

Commit c3dbbdb

Browse files
Add API ID
1 parent 179ea69 commit c3dbbdb

File tree

8 files changed

+104
-23
lines changed

8 files changed

+104
-23
lines changed

samples/general/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ Sets up a simple APIM instance with a variety of policies to experiment.
1212
1. Become proficient with how policies operate.
1313
1. Gain confidence in setting up and configuring policies appropriately.
1414

15+
## 🔗 APIs
16+
17+
| API Name | What does it do? |
18+
|:------------------------------|:----------------------------------------------------------------------------------------------------------------------------------|
19+
| Request Headers | Returns a list of the request headers sent to APIM. Useful for debugging. |
20+
| API ID | Returns the ID of an API as per a predefined standard such as `api-123`. Useful for explicitly identifying an API in telemetry. |
21+
1522
## ⚙️ Configuration
1623

1724
1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use.
18-
1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md.
19-
1. If the infrastructure _does_ exist, adjust the `user-defined parameters` in the _Initialize notebook variables_ below. Please ensure that all parameters match your infrastructure.
25+
1. Press `Run All` in this sample's `create.ipynb` notebook.

samples/general/create.ipynb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,20 @@
4242
"\n",
4343
"# Define the APIs and their operations and policies\n",
4444
"\n",
45-
"# API 1\n",
46-
"api1_path = f'{api_prefix}api1'\n",
47-
"api1_get = GET_APIOperation('This is a GET for API 1')\n",
48-
"api1_post = POST_APIOperation('This is a POST for API 1')\n",
49-
"api1 = API(api1_path, 'API 1', api1_path, 'This is API 1', operations = [api1_get, api1_post], tags = tags)\n",
50-
"\n",
51-
"# API 2\n",
52-
"api2_path = f'{api_prefix}api2'\n",
53-
"api2_post = POST_APIOperation('This is a POST for API 2')\n",
54-
"api2 = API(api2_path, 'API 2', api2_path, 'This is API 2', operations = [api2_post], tags = tags)\n",
55-
"\n",
56-
"# API 3: Request Headers\n",
45+
"# API 1: Request Headers\n",
5746
"rh_path = f'{api_prefix}request-headers'\n",
5847
"pol_request_headers_get = utils.read_policy_xml(REQUEST_HEADERS_XML_POLICY_PATH)\n",
5948
"request_headers_get = GET_APIOperation('Gets the request headers for the current request and returns them. Great for troubleshooting.', pol_request_headers_get)\n",
6049
"request_headers = API(rh_path, 'Request Headers', rh_path, 'API for request headers', operations = [request_headers_get], tags = tags)\n",
6150
"\n",
51+
"# API 2: API ID: Extract and trace API Identifier\n",
52+
"api_id_path = f'{api_prefix}api-id'\n",
53+
"pol_api_id_get = utils.read_policy_xml(API_ID_XML_POLICY_PATH)\n",
54+
"api_id_get = GET_APIOperation('Gets the API identifier for the current request and traces it', pol_api_id_get)\n",
55+
"api_id = API(api_id_path, 'API Identifier (api-42)', api_id_path, 'API for extracting and tracing API identifier', operations = [api_id_get], tags = tags)\n",
56+
"\n",
6257
"# APIs Array\n",
63-
"apis: List[API] = [api1, api2, request_headers]\n",
58+
"apis: List[API] = [request_headers, api_id]\n",
6459
"\n",
6560
"utils.print_ok('Notebook initialized')"
6661
]
@@ -121,16 +116,22 @@
121116
"\n",
122117
"# Initialize testing framework\n",
123118
"tests = ApimTesting(\"General Sample Tests\", sample_folder, nb_helper.deployment)\n",
124-
"api_subscription_key = apim_apis[2]['subscriptionPrimaryKey']\n",
125119
"\n",
126120
"# Preflight: Check if the infrastructure architecture deployment uses Azure Front Door. If so, assume that APIM is not directly accessible and use the Front Door URL instead.\n",
127121
"endpoint_url = utils.test_url_preflight_check(deployment, rg_name, apim_gateway_url)\n",
128122
"\n",
129123
"# 1) Request Headers\n",
124+
"api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n",
130125
"reqs = ApimRequests(endpoint_url, api_subscription_key)\n",
131-
"output = reqs.singleGet('/request-headers', msg = 'Calling Request Headers API via Azure Front Door. Expect 200.')\n",
126+
"output = reqs.singleGet('/request-headers', msg = 'Calling Request Headers API. Expect 200.')\n",
132127
"tests.verify('Host:' in output, True)\n",
133128
"\n",
129+
"# 2) API ID\n",
130+
"api_subscription_key = apim_apis[1]['subscriptionPrimaryKey']\n",
131+
"reqs = ApimRequests(endpoint_url, api_subscription_key)\n",
132+
"output = reqs.singleGet('/api-id', msg = 'Calling API ID API. Expect 200.')\n",
133+
"tests.verify(output, 'Extracted API ID: api-42')\n",
134+
"\n",
134135
"tests.print_summary()\n",
135136
"\n",
136137
"utils.print_ok('All done!')"
@@ -158,4 +159,4 @@
158159
},
159160
"nbformat": 4,
160161
"nbformat_minor": 2
161-
}
162+
}

shared/apim-policies/api-id.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
This policy sample demonstrates the listing of all inbound HTTP headers, which can be a bit more straight-forward than setting up a trace.
3+
-->
4+
<policies>
5+
<inbound>
6+
<base />
7+
<include-fragment fragment-id="Api-Id" />
8+
<return-response>
9+
<set-status code="200" reason="OK" />
10+
<set-body>
11+
@{
12+
var apiId = context.Variables.GetValueOrDefault<string>("apiId", string.Empty);
13+
return (string.IsNullOrEmpty(apiId)) ? "No API ID extracted." : string.Format("Extracted API ID: {0}", apiId);
14+
}
15+
</set-body>
16+
</return-response>
17+
</inbound>
18+
<backend>
19+
<base />
20+
</backend>
21+
<outbound>
22+
<base />
23+
</outbound>
24+
<on-error>
25+
<base />
26+
</on-error>
27+
</policies>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!--
2+
Policy Fragment: Extract API ID from API Name
3+
4+
Tags are very useful to explicitly identify an API as API names or paths may be ambiguous and repetitive. Having explicit IDs can significantly aid with telemetry.
5+
As there is no way yet to extract a tag associated with an API in Azure API Management using policies, putting such information in the API name is a workable alternative.
6+
If the format is consistent, it can be parsed and used in the policies.
7+
8+
The format we are looking for is `<api-name> (api-246)`, where the value in parentheses starts with "api-" followed by numeric characters.
9+
10+
This fragment extracts the API ID from context.Api.Name and stores it in the "apiId" variable.
11+
If the format doesn't match the expected pattern, the variable will be set to an empty string.
12+
13+
Expected format examples:
14+
- "User Management API (api-246)" → extracts "api-246"
15+
- "Order Service (api-123)" → extracts "api-123"
16+
- "Payment Gateway (payment-456)" → ignored (doesn't start with "api-")
17+
- "Simple API" → ignored (no parentheses)
18+
19+
https://learn.microsoft.com/azure/api-management/api-management-policy-expressions#ref-context-api
20+
-->
21+
<fragment>
22+
<set-variable name="apiId" value="@{
23+
// Use regex to match the pattern: (api-\d+) at the end of the string
24+
var match = System.Text.RegularExpressions.Regex.Match(context.Api.Name, @"\(api-\d+\)$");
25+
26+
// Extract the content within parentheses, removing the parentheses
27+
return (match.Success) ? match.Value.Substring(1, match.Value.Length - 2) : string.Empty;
28+
}" />
29+
30+
<choose>
31+
<when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("apiId", string.Empty)))">
32+
<trace source="API Id Policy Fragment">
33+
<message>@{
34+
return "Extracted API ID: " + (string)context.Variables["apiId"];
35+
}</message>
36+
<metadata name="ApiId" value="@((string)context.Variables["apiId"])" />
37+
</trace>
38+
<!-- Set a header for downstream services to use -->
39+
<set-header name="X-Api-Id" exists-action="override">
40+
<value>@((string)context.Variables["apiId"])</value>
41+
</set-header>
42+
</when>
43+
</choose>
44+
</fragment>

shared/python/apimtesting.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def verify(self, value: any, expected: any) -> bool:
5151
bool: True, if the assertion passes; otherwise, False.
5252
"""
5353
try:
54+
print(f'🔍 Assert that [{value}] matches [{expected}].')
5455
self.total_tests += 1
5556
assert value == expected, f'Value [{value}] does not match expected [{expected}]'
5657
self.tests_passed += 1

shared/python/apimtypes.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ def _get_project_root() -> Path:
3636
_SHARED_XML_POLICY_BASE_PATH = _PROJECT_ROOT / 'shared' / 'apim-policies'
3737

3838
# Policy file paths (now absolute and platform-independent)
39-
DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml')
40-
HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml')
41-
REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml')
42-
BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml')
39+
DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml')
40+
HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml')
41+
REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml')
42+
BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml')
43+
API_ID_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'api-id.xml')
4344

4445
SUBSCRIPTION_KEY_PARAMETER_NAME = 'api-key'
4546
SLEEP_TIME_BETWEEN_REQUESTS_MS = 50

shared/python/infrastructures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def _define_policy_fragments(self) -> List[PolicyFragment]:
5959

6060
# The base policy fragments common to all infrastructures
6161
self.base_pfs = [
62+
PolicyFragment('Api-Id', utils.read_policy_xml(utils.determine_shared_policy_path('pf-api-id.xml')), 'Extracts a specific API identifier for tracing.'),
6263
PolicyFragment('AuthZ-Match-All', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-all.xml')), 'Authorizes if all of the specified roles match the JWT role claims.'),
6364
PolicyFragment('AuthZ-Match-Any', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-any.xml')), 'Authorizes if any of the specified roles match the JWT role claims.'),
6465
PolicyFragment('Http-Response-200', utils.read_policy_xml(utils.determine_shared_policy_path('pf-http-response-200.xml')), 'Returns a 200 OK response for the current HTTP method.'),

shared/python/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def _query_and_select_infrastructure(self) -> tuple[INFRASTRUCTURE | None, int |
473473
option_counter += 1
474474
else:
475475
print_warning('No existing supported infrastructures found.')
476-
print_info(f'🚀 Automatically proceeding to create new infrastructure: {self.deployment.value}')
476+
print_info(f'Automatically proceeding to create new infrastructure: {self.deployment.value}')
477477

478478
# Automatically create the desired infrastructure without user confirmation
479479
selected_index = self._get_current_index()

0 commit comments

Comments
 (0)