Skip to content

Commit d8c0249

Browse files
committed
refactor: enforce single thing type per thing (AWS IoT limitation)
AWS IoT Core allows only one thing type per thing. Refactored codebase to remove plural thing type support and enforce singular thing type. Changes: - Removed IoTThingTypes parameter and THING_TYPE_NAMES env var - Updated product_verifier to only handle THING_TYPE_NAME (singular) - Simplified bulk_importer to process single thing type - Updated E2E test framework for singular thing type validation - Fixed unit test isolation issues (env var cleanup in tearDown) - Updated all documentation (MULTI_ATTACHMENT_GUIDE.md and vendor docs) Breaking change: IoTThingTypes parameter removed. Use IoTThingType instead. All unit tests passing (160 passed, 1 xpassed)
1 parent f5205cc commit d8c0249

File tree

11 files changed

+76
-83
lines changed

11 files changed

+76
-83
lines changed

docs/MULTI_ATTACHMENT_GUIDE.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
Thingpress now supports attaching multiple policies, thing groups, and thing types to each certificate/thing. This enables enterprise IoT deployment patterns with hierarchical organization.
5+
Thingpress supports attaching multiple policies and thing groups to each certificate/thing. **Note: AWS IoT Core allows only one thing type per thing.**
66

77
## Parameter Syntax
88

@@ -14,7 +14,7 @@ Use comma-delimited lists for multiple values:
1414
sam deploy --parameter-overrides \
1515
IoTPolicies=policy1,policy2,policy3 \
1616
IoTThingGroups=dept-eng,location-seattle,product-sensor \
17-
IoTThingTypes=temp-sensor,humidity-sensor
17+
IoTThingType=temp-sensor
1818
```
1919

2020
### Legacy Single-Value Parameters (Still Supported)
@@ -28,6 +28,8 @@ sam deploy --parameter-overrides \
2828
IoTThingType=my-type
2929
```
3030

31+
**Note:** Thing type is always singular - AWS IoT Core allows only one thing type per thing.
32+
3133
## Common Use Cases
3234

3335
### 1. Organizational Hierarchy
@@ -53,10 +55,10 @@ Applies multiple policies:
5355
### 3. Device Categorization
5456

5557
```bash
56-
IoTThingTypes=hardware-esp32,firmware-v2.1,capability-temperature
58+
IoTThingType=hardware-esp32
5759
```
5860

59-
Multiple classifications:
61+
Single classification per device (AWS IoT limitation):
6062
- Hardware model
6163
- Firmware version
6264
- Device capabilities
@@ -104,16 +106,20 @@ IoTThingGroups=acme-corp,manufacturing,quality-control,seattle-plant,sensor-netw
104106

105107
### Thing Type Strategy
106108

107-
Use for device characteristics:
109+
**AWS IoT Limitation:** Each thing can have only one thing type.
110+
111+
Use thing types to categorize device characteristics:
108112
```bash
109-
IoTThingTypes=hardware-esp32,sensor-temperature,protocol-mqtt
113+
IoTThingType=sensor-temperature
114+
# OR
115+
IoTThingType=hardware-esp32
110116
```
111117

112118
## Recommended Limits
113119

114120
- **Policies**: Maximum 5 per certificate
115121
- **Thing Groups**: Maximum 10 per thing
116-
- **Thing Types**: Maximum 3 per thing
122+
- **Thing Type**: Exactly 1 per thing (AWS IoT limitation)
117123

118124
Exceeding these limits may impact performance.
119125

@@ -199,23 +205,23 @@ sam deploy --parameter-overrides \
199205
```bash
200206
IoTPolicies=base-mqtt,manufacturing-floor,quality-control
201207
IoTThingGroups=factory-seattle,line-assembly-1,zone-welding
202-
IoTThingTypes=plc-controller,sensor-vibration
208+
IoTThingType=plc-controller
203209
```
204210

205211
### Smart Buildings
206212

207213
```bash
208214
IoTPolicies=base-connectivity,building-automation,hvac-control
209215
IoTThingGroups=building-hq,floor-3,zone-west-wing
210-
IoTThingTypes=thermostat,occupancy-sensor
216+
IoTThingType=thermostat
211217
```
212218

213219
### Fleet Management
214220

215221
```bash
216222
IoTPolicies=base-telemetry,fleet-tracking,geofence-enabled
217223
IoTThingGroups=fleet-delivery,region-west,vehicle-type-van
218-
IoTThingTypes=gps-tracker,obd-reader
224+
IoTThingType=gps-tracker
219225
```
220226

221227
## API Reference
@@ -260,5 +266,6 @@ For issues or questions:
260266

261267
## Version History
262268

263-
- **v1.0.1**: Added multiple policies, thing groups, and thing types support
269+
- **v1.1.0**: Removed IoTThingTypes parameter (AWS IoT allows only one thing type per thing)
270+
- **v1.0.1**: Added multiple policies and thing groups support
264271
- **v1.0.0**: Initial release with single-value parameters

docs/vendors/espressif.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ $ sam deploy \
9292
--parameter-overrides \
9393
IoTPolicies=base-connectivity,sensor-telemetry,admin-access \
9494
IoTThingGroups=dept-engineering,location-seattle,product-sensor \
95-
IoTThingTypes=esp32-s3 \
95+
IoTThingType=esp32-s3 \
9696
--capabilities CAPABILITY_NAMED_IAM
9797
```
9898

docs/vendors/generated.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ $ sam deploy \
9090
--parameter-overrides \
9191
IoTPolicies=base-connectivity,sensor-telemetry,admin-access \
9292
IoTThingGroups=dept-engineering,location-seattle,product-sensor \
93-
IoTThingTypes=custom-device \
93+
IoTThingType=custom-device \
9494
--capabilities CAPABILITY_NAMED_IAM
9595
```
9696

docs/vendors/infineon.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ $ sam deploy \
9595
--parameter-overrides \
9696
IoTPolicies=base-connectivity,sensor-telemetry,admin-access \
9797
IoTThingGroups=dept-engineering,location-seattle,product-sensor \
98-
IoTThingTypes=optiga-trust-m \
98+
IoTThingType=optiga-trust-m \
9999
--capabilities CAPABILITY_NAMED_IAM
100100
```
101101

docs/vendors/microchip.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ $ sam deploy \
8383
--parameter-overrides \
8484
IoTPolicies=base-connectivity,sensor-telemetry,admin-access \
8585
IoTThingGroups=dept-engineering,location-seattle,product-sensor \
86-
IoTThingTypes=atecc608b \
86+
IoTThingType=atecc608b \
8787
--capabilities CAPABILITY_NAMED_IAM
8888
```
8989

src/bulk_importer/bulk_importer/main.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,12 @@ def process_sqs(config, session: Session=default_session):
148148
thing_arn=thing_arn,
149149
session=session)
150150

151-
# Process multiple thing types (backward compatible)
152-
thing_types = config.get('thing_types', [])
153-
if thing_types:
154-
for thing_type_name in thing_types:
155-
process_thing_type(thing_name=config.get(ImporterMessageKey.THING_NAME.value),
156-
thing_type_name=thing_type_name,
157-
session=session)
158-
else:
159-
# Legacy single thing type support
160-
thing_type_name = config.get(ImporterMessageKey.THING_TYPE_NAME.value)
161-
if thing_type_name:
162-
process_thing_type(thing_name=config.get(ImporterMessageKey.THING_NAME.value),
163-
thing_type_name=thing_type_name,
164-
session=session)
151+
# Process thing type (singular - AWS IoT allows only one thing type per thing)
152+
thing_type_name = config.get(ImporterMessageKey.THING_TYPE_NAME.value)
153+
if thing_type_name:
154+
process_thing_type(thing_name=config.get(ImporterMessageKey.THING_NAME.value),
155+
thing_type_name=thing_type_name,
156+
session=session)
165157

166158
return {
167159
"certificate_id": certificate_id,

src/product_verifier/main.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,24 @@ def lambda_handler(event,
6262
QUEUE_TARGET_ESPRESSIF, QUEUE_TARGET_INFINEON, QUEUE_TARGET_MICROCHIP, QUEUE_TARGET_GENERATED
6363
6464
Supports both new multi-value and legacy single-value parameters:
65-
New: POLICY_NAMES, THING_GROUP_NAMES, THING_TYPE_NAMES (comma-delimited)
66-
Legacy: POLICY_NAME, THING_GROUP_NAME, THING_TYPE_NAME (single values)
65+
New: POLICY_NAMES, THING_GROUP_NAMES (comma-delimited)
66+
Legacy: POLICY_NAME, THING_GROUP_NAME (single values)
67+
Thing Type: THING_TYPE_NAME (always singular - AWS IoT limitation)
6768
"""
6869
config = {}
6970

7071
# Try new multi-value parameters first, fall back to legacy
7172
e_policies = os.environ.get('POLICY_NAMES', '')
7273
e_thing_groups = os.environ.get('THING_GROUP_NAMES', '')
73-
e_thing_types = os.environ.get('THING_TYPE_NAMES', '')
7474

7575
# Backward compatibility: if new params empty, try legacy
7676
if not e_policies:
7777
e_policies = os.environ.get('POLICY_NAME', '')
7878
if not e_thing_groups:
7979
e_thing_groups = os.environ.get('THING_GROUP_NAME', '')
80-
if not e_thing_types:
81-
e_thing_types = os.environ.get('THING_TYPE_NAME', '')
80+
81+
# Thing type is always singular (AWS IoT limitation: one thing type per thing)
82+
e_thing_type = os.environ.get('THING_TYPE_NAME', '')
8283

8384
# Handle both raw dict and S3Event object formats
8485
if hasattr(event, 'records'):
@@ -116,16 +117,10 @@ def lambda_handler(event,
116117
if thing_groups:
117118
config['thing_groups'] = thing_groups
118119

119-
# Parse and validate thing types
120-
thing_type_names = parse_comma_delimited_list(e_thing_types)
121-
if thing_type_names:
122-
thing_types = []
123-
for thing_type_name in thing_type_names:
124-
if check_cfn_prop_valid(thing_type_name):
125-
get_thing_type_arn(thing_type_name, default_session)
126-
thing_types.append(thing_type_name)
127-
if thing_types:
128-
config['thing_types'] = thing_types
120+
# Parse and validate thing type (singular - AWS IoT allows only one thing type per thing)
121+
if e_thing_type and check_cfn_prop_valid(e_thing_type):
122+
get_thing_type_arn(e_thing_type, default_session)
123+
config['thing_type_name'] = e_thing_type
129124

130125
try:
131126
queue_url = get_provider_queue(config['bucket'])

template.yaml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,7 @@ Parameters:
5959
IoTThingType:
6060
Default: None
6161
Type: String
62-
Description: (DEPRECATED - Use IoTThingTypes) Single Thing Type for backward compatibility.
63-
64-
IoTThingTypes:
65-
Default: None
66-
Type: CommaDelimitedList
67-
Description: Comma-separated list of AWS IoT Thing Types.
68-
Use 'None' for no thing types. Example sensor-temp,sensor-humidity
62+
Description: AWS IoT Thing Type to apply to things. Use 'None' for no thing type.
6963

7064
InfineonCertBundleType:
7165
Default: E0E0
@@ -394,7 +388,6 @@ Resources:
394388
QUEUE_TARGET_GENERATED: !Ref ThingpressGeneratedProviderQueue
395389
POLICY_NAMES: !Join [',', !Ref IoTPolicies]
396390
THING_GROUP_NAMES: !Join [',', !Ref IoTThingGroups]
397-
THING_TYPE_NAMES: !Join [',', !Ref IoTThingTypes]
398391
POLICY_NAME: !Ref IoTPolicy
399392
THING_GROUP_NAME: !Ref IoTThingGroup
400393
THING_TYPE_NAME: !Ref IoTThingType

test/integration/end_to_end/e2e_test_framework.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,8 @@ def _add_validation_summary(self):
526526
'exact_match_count': validation_details.get('summary', {}).get('correct_thing_groups', 0),
527527
'count_mismatch_count': validation_details.get('summary', {}).get('thing_group_count_mismatches', 0)
528528
},
529-
'thing_types': {
530-
'expected': self.expected_config.get('thing_types', []),
529+
'thing_type': {
530+
'expected': self.expected_config.get('thing_type'),
531531
'correct_count': validation_details.get('summary', {}).get('correct_thing_types', 0),
532532
'incorrect_count': len(validation_details.get('summary', {}).get('incorrect_thing_types', []))
533533
}
@@ -564,12 +564,12 @@ def _log_validation_summary(self):
564564
if groups['count_mismatch_count'] > 0:
565565
self.logger.warning(f" ⚠️ Count Mismatches: {groups['count_mismatch_count']}")
566566

567-
# Thing Types
568-
types = summary['thing_types']
569-
self.logger.info(f"Thing Types (Expected: {types['expected']})")
570-
self.logger.info(f" Correct: {types['correct_count']}/{summary['total_things']}")
571-
if types['incorrect_count'] > 0:
572-
self.logger.warning(f" ⚠️ Incorrect: {types['incorrect_count']}")
567+
# Thing Type (singular)
568+
thing_type = summary['thing_type']
569+
self.logger.info(f"Thing Type (Expected: {thing_type['expected']})")
570+
self.logger.info(f" Correct: {thing_type['correct_count']}/{summary['total_things']}")
571+
if thing_type['incorrect_count'] > 0:
572+
self.logger.warning(f" ⚠️ Incorrect: {thing_type['incorrect_count']}")
573573

574574
self.logger.info("=" * 60)
575575

@@ -605,7 +605,7 @@ def _get_expected_config_from_stack(self) -> dict:
605605
expected_config = {
606606
'policies': [],
607607
'thing_groups': [],
608-
'thing_types': []
608+
'thing_type': None # Singular - AWS IoT allows only one thing type per thing
609609
}
610610

611611
# Extract configuration from stack parameters
@@ -634,23 +634,17 @@ def _get_expected_config_from_stack(self) -> dict:
634634
if not expected_config['thing_groups']:
635635
expected_config['thing_groups'] = [param_value]
636636

637-
# Handle thing types
638-
elif param_key == 'IoTThingTypes' and param_value and param_value != 'None':
639-
expected_config['thing_types'] = [
640-
t.strip() for t in param_value.split(',')
641-
if t.strip() and t.strip() != 'None'
642-
]
637+
# Handle thing type (singular - AWS IoT allows only one thing type per thing)
643638
elif param_key == 'IoTThingType' and param_value and param_value != 'None':
644-
if not expected_config['thing_types']:
645-
expected_config['thing_types'] = [param_value]
639+
expected_config['thing_type'] = param_value
646640

647641
self.logger.info(f"Expected config from stack: {expected_config}")
648642
return expected_config
649643

650644
except Exception as e:
651645
self.logger.error(f"Failed to get expected config from stack: {e}")
652646
# Return empty config rather than failing
653-
return {'policies': [], 'thing_groups': [], 'thing_types': []}
647+
return {'policies': [], 'thing_groups': [], 'thing_type': None}
654648

655649
def run_test(self, timeout_minutes: int = 10) -> dict:
656650
"""Run the complete end-to-end test for this provider"""
@@ -723,7 +717,7 @@ def _validate_iot_things(self, iot_things: list[dict]) -> dict:
723717

724718
expected_policies = set(self.expected_config.get('policies', []))
725719
expected_groups = set(self.expected_config.get('thing_groups', []))
726-
expected_types = self.expected_config.get('thing_types', [])
720+
expected_type = self.expected_config.get('thing_type') # Singular
727721

728722
validation_results = {
729723
'valid': True,
@@ -766,8 +760,8 @@ def _validate_iot_things(self, iot_things: list[dict]) -> dict:
766760
extra_groups = thing_groups - expected_groups
767761
group_count_match = len(thing_groups) == len(expected_groups)
768762

769-
# EXACT MATCH: Check thing type applied to thing
770-
type_match = thing_type in expected_types if expected_types else True
763+
# EXACT MATCH: Check thing type applied to thing (singular)
764+
type_match = thing_type == expected_type if expected_type else (thing_type is None)
771765

772766
thing_validation = {
773767
'thing_name': thing['thingName'],
@@ -792,7 +786,7 @@ def _validate_iot_things(self, iot_things: list[dict]) -> dict:
792786
'extra': list(extra_groups)
793787
},
794788
'thing_type': {
795-
'expected': expected_types,
789+
'expected': expected_type,
796790
'actual': thing_type,
797791
'match': type_match
798792
},

test/unit/test_bulk_importer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,12 @@ def test_process_sqs_empty_lists(self):
343343
"""Test processing with empty policies and thing groups lists"""
344344
from bulk_importer.main import process_sqs
345345

346-
# Config with empty lists
346+
# Config with empty lists and no thing type
347347
config = {
348348
'certificate': self.local_cert_loaded,
349349
'thing': 'test-thing-no-attachments',
350350
'policies': [],
351-
'thing_groups': [],
352-
'thing_types': []
351+
'thing_groups': []
353352
}
354353

355354
# Execute

0 commit comments

Comments
 (0)