From 4485ba8a2351917b500d0cf9cfe00d9ebf0e77ce Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:35:13 -0400 Subject: [PATCH 01/65] aws - add s3files file-system, mount-target and access-point resources (#10972) * aws - add s3files file-system, mount-target and access-point resources * exempt s3files-mount-target from arn info meta lint * address review feedback: drop redundant policy_attribute init, remove access-point-account filter, align parent annotation * drop redundant permission_prefix matching service default * use universal tagging for file-system and access-point resources --------- Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> --- c7n/resources/resource_map.py | 3 + c7n/resources/s3files.py | 218 ++++++++++++++++++ .../s3files.ListAccessPoints_1.json | 27 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../tagging.GetResources_1.json | 18 ++ .../s3files.ListAccessPoints_1.json | 27 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.ListTagsForResource_1.json | 16 ++ .../tagging.GetResources_1.json | 18 ++ .../tagging.TagResources_1.json | 7 + .../s3files.GetFileSystemPolicy_1.json | 8 + .../s3files.GetFileSystem_1.json | 30 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.GetFileSystem_1.json | 30 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.GetFileSystem_1.json | 30 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.ListTagsForResource_1.json | 16 ++ .../tagging.TagResources_1.json | 7 + .../s3files.GetFileSystemPolicy_1.json | 8 + .../s3files.GetFileSystem_1.json | 30 +++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.GetMountTarget_1.json | 18 ++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.ListMountTargets_1.json | 19 ++ .../ec2.DescribeSecurityGroups_1.json | 190 +++++++++++++++ .../s3files.GetMountTarget_1.json | 18 ++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.ListMountTargets_1.json | 19 ++ .../ec2.DescribeSubnets_1.json | 44 ++++ .../s3files.GetMountTarget_1.json | 18 ++ .../s3files.ListFileSystems_1.json | 26 +++ .../s3files.ListMountTargets_1.json | 19 ++ .../s3files.GetFileSystemPolicy_1.json | 12 + .../s3files.GetFileSystem_1.json | 30 +++ .../s3files.ListFileSystems_1.json | 26 +++ tests/test_policy.py | 2 +- tests/test_s3files.py | 147 ++++++++++++ 38 files changed, 1288 insertions(+), 1 deletion(-) create mode 100644 c7n/resources/s3files.py create mode 100644 tests/data/placebo/test_s3files_access_point_query/s3files.ListAccessPoints_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_query/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_query/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_tag/s3files.ListAccessPoints_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_tag/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_tag/s3files.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3files_access_point_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystemPolicy_1.json create mode 100644 tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystem_1.json create mode 100644 tests/data/placebo/test_s3files_cross_account/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_query/s3files.GetFileSystem_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_query/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_tag/s3files.GetFileSystem_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_tag/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_tag/s3files.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_s3files_file_system_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystemPolicy_1.json create mode 100644 tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystem_1.json create mode 100644 tests/data/placebo/test_s3files_has_statement/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_query/s3files.GetMountTarget_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_query/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_query/s3files.ListMountTargets_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_security_group/ec2.DescribeSecurityGroups_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_security_group/s3files.GetMountTarget_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListMountTargets_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_subnet/ec2.DescribeSubnets_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_subnet/s3files.GetMountTarget_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListFileSystems_1.json create mode 100644 tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListMountTargets_1.json create mode 100644 tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystemPolicy_1.json create mode 100644 tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystem_1.json create mode 100644 tests/data/placebo/test_s3files_no_policy/s3files.ListFileSystems_1.json create mode 100644 tests/test_s3files.py diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index a94cfad7e6d..5e03b60190a 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -305,6 +305,9 @@ "aws.rrset": "c7n.resources.route53.ResourceRecordSet", "aws.s3": "c7n.resources.s3.S3", "aws.s3-directory": "c7n.resources.s3.S3Directory", + "aws.s3files-access-point": "c7n.resources.s3files.AccessPoint", + "aws.s3files-file-system": "c7n.resources.s3files.FileSystem", + "aws.s3files-mount-target": "c7n.resources.s3files.MountTarget", "aws.s3-access-point": "c7n.resources.s3control.AccessPoint", "aws.s3-access-point-multi": "c7n.resources.s3control.MultiRegionAccessPoint", "aws.s3-storage-lens": "c7n.resources.s3control.StorageLens", diff --git a/c7n/resources/s3files.py b/c7n/resources/s3files.py new file mode 100644 index 00000000000..feb88abd2aa --- /dev/null +++ b/c7n/resources/s3files.py @@ -0,0 +1,218 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from c7n.filters import CrossAccountAccessFilter +from c7n.filters.policystatement import HasStatementFilter +from c7n.filters.vpc import SecurityGroupFilter, SubnetFilter, NetworkLocation +from c7n.manager import resources +from c7n.query import ( + ChildDescribeSource, + ChildResourceManager, + DescribeSource, + QueryResourceManager, + TypeInfo, +) +from c7n.tags import universal_augment +from c7n.utils import local_session + + +class DescribeFileSystem(DescribeSource): + + def augment(self, resources): + resources = super().augment(resources) + for r in resources: + r['Tags'] = [ + {'Key': t['key'], 'Value': t['value']} + for t in r.pop('tags', [])] + return resources + + +@resources.register('s3files-file-system') +class FileSystem(QueryResourceManager): + """AWS S3 Files - File System + + An S3 Files file system provides file system interface access to + the data of a backing general purpose S3 bucket. + + https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html + """ + + class resource_type(TypeInfo): + service = 's3files' + enum_spec = ('list_file_systems', 'fileSystems', None) + detail_spec = ('get_file_system', 'fileSystemId', 'fileSystemId', None) + arn = 'fileSystemArn' + id = 'fileSystemId' + name = 'name' + date = 'creationTime' + cfn_type = 'AWS::S3Files::FileSystem' + universal_taggable = object() + + source_mapping = {'describe': DescribeFileSystem} + + +@resources.register('s3files-mount-target') +class MountTarget(ChildResourceManager): + """AWS S3 Files - Mount Target + + Mount targets are the VPC network interfaces through which an + S3 Files file system is reachable over NFS. + + :example: + + .. code-block:: yaml + + policies: + - name: s3files-mount-target-subnet + resource: aws.s3files-mount-target + filters: + - type: subnet + key: State + value: available + """ + + class resource_type(TypeInfo): + service = 's3files' + parent_spec = ('s3files-file-system', 'fileSystemId', None) + enum_spec = ('list_mount_targets', 'mountTargets', None) + detail_spec = ('get_mount_target', 'mountTargetId', 'mountTargetId', None) + name = id = 'mountTargetId' + arn = False + cfn_type = 'AWS::S3Files::MountTarget' + + +@MountTarget.filter_registry.register('subnet') +class MountTargetSubnetFilter(SubnetFilter): + + RelatedIdsExpression = "subnetId" + + +@MountTarget.filter_registry.register('security-group') +class MountTargetSecurityGroupFilter(SecurityGroupFilter): + + RelatedIdsExpression = "securityGroups[]" + + +MountTarget.filter_registry.register('network-location', NetworkLocation) + + +class DescribeAccessPoint(ChildDescribeSource): + + def augment(self, resources): + return universal_augment(self.manager, super().augment(resources)) + + +@resources.register('s3files-access-point') +class AccessPoint(ChildResourceManager): + """AWS S3 Files - Access Point + + S3 Files access points are EFS-style entry points into a file + system, pinning NFS clients to a POSIX identity and root + directory. They carry no resource policy and no public access + configuration; file system access is governed by the file system + policy (see the ``cross-account`` and ``has-statement`` + filters on ``aws.s3files-file-system``). + + :example: + + .. code-block:: yaml + + policies: + - name: s3files-access-point-root-user + resource: aws.s3files-access-point + filters: + - type: value + key: posixUser.uid + value: 0 + """ + + class resource_type(TypeInfo): + service = 's3files' + parent_spec = ('s3files-file-system', 'fileSystemId', None) + enum_spec = ('list_access_points', 'accessPoints', None) + arn = 'accessPointArn' + id = 'accessPointId' + name = 'name' + cfn_type = 'AWS::S3Files::AccessPoint' + universal_taggable = object() + + source_mapping = {'describe-child': DescribeAccessPoint} + + +class FileSystemPolicyMixin: + """Annotates file systems with their resource policy under c7n:Policy.""" + + policy_attribute = 'c7n:Policy' + + def policy_annotate(self, client, resource): + if self.policy_attribute in resource: + return resource + try: + resp = client.get_file_system_policy( + fileSystemId=resource['fileSystemId']) + resource[self.policy_attribute] = resp.get('policy') + except client.exceptions.ResourceNotFoundException: + resource[self.policy_attribute] = None + return resource + + +@FileSystem.filter_registry.register('cross-account') +class FileSystemCrossAccount(FileSystemPolicyMixin, CrossAccountAccessFilter): + """Filter file systems whose policy grants access outside of + allowed accounts or organizations. + + A file system without a policy does not evaluate IAM for NFS + access at all; combine with ``has-statement`` to require a policy + to be attached. + + :example: + + .. code-block:: yaml + + policies: + - name: s3files-cross-account + resource: aws.s3files-file-system + filters: + - type: cross-account + whitelist_orgids: + - o-xxxxxxxxxx + """ + permissions = ('s3files:GetFileSystemPolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3files') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + +@FileSystem.filter_registry.register('has-statement') +class FileSystemHasStatement(FileSystemPolicyMixin, HasStatementFilter): + """Find file systems with matching file system policy statements. + + :example: + + .. code-block:: yaml + + policies: + - name: s3files-require-secure-transport + resource: aws.s3files-file-system + filters: + - type: has-statement + statements: + - Effect: Deny + Condition: + Bool: + "aws:SecureTransport": "false" + """ + permissions = ('s3files:GetFileSystemPolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3files') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + def get_std_format_args(self, fs): + return { + 'file_system_arn': fs['fileSystemArn'], + 'account_id': self.manager.config.account_id, + 'region': self.manager.config.region, + } diff --git a/tests/data/placebo/test_s3files_access_point_query/s3files.ListAccessPoints_1.json b/tests/data/placebo/test_s3files_access_point_query/s3files.ListAccessPoints_1.json new file mode 100644 index 00000000000..41b62372fde --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_query/s3files.ListAccessPoints_1.json @@ -0,0 +1,27 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "accessPoints": [ + { + "accessPointArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4/access-point/fsap-0e635ea792b509a43", + "accessPointId": "fsap-0e635ea792b509a43", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "status": "available", + "ownerId": "644160558196", + "posixUser": { + "uid": 1000, + "gid": 1000 + }, + "rootDirectory": { + "path": "/data", + "creationPermissions": { + "ownerUid": 1000, + "ownerGid": 1000, + "permissions": "0755" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_query/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_access_point_query/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..d40a8249773 --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_query/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 1, + "minute": 17, + "second": 27, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_query/tagging.GetResources_1.json b/tests/data/placebo/test_s3files_access_point_query/tagging.GetResources_1.json new file mode 100644 index 00000000000..0b2c9631106 --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_query/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4/access-point/fsap-0e635ea792b509a43", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_tag/s3files.ListAccessPoints_1.json b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListAccessPoints_1.json new file mode 100644 index 00000000000..41b62372fde --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListAccessPoints_1.json @@ -0,0 +1,27 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "accessPoints": [ + { + "accessPointArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4/access-point/fsap-0e635ea792b509a43", + "accessPointId": "fsap-0e635ea792b509a43", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "status": "available", + "ownerId": "644160558196", + "posixUser": { + "uid": 1000, + "gid": 1000 + }, + "rootDirectory": { + "path": "/data", + "creationPermissions": { + "ownerUid": 1000, + "ownerGid": 1000, + "permissions": "0755" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_tag/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..d40a8249773 --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 1, + "minute": 17, + "second": 27, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_tag/s3files.ListTagsForResource_1.json b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListTagsForResource_1.json new file mode 100644 index 00000000000..3d743dab55b --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_tag/s3files.ListTagsForResource_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "Env", + "value": "test" + }, + { + "key": "Owner", + "value": "c7n-test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_tag/tagging.GetResources_1.json b/tests/data/placebo/test_s3files_access_point_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..0b2c9631106 --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_tag/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4/access-point/fsap-0e635ea792b509a43", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_access_point_tag/tagging.TagResources_1.json b/tests/data/placebo/test_s3files_access_point_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_s3files_access_point_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystemPolicy_1.json b/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystemPolicy_1.json new file mode 100644 index 00000000000..370138200c6 --- /dev/null +++ b/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystemPolicy_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystemId": "fs-02d120d4ae6209b2d", + "policy": "{\n \"Version\" : \"2012-10-17\",\n \"Statement\" : [ {\n \"Sid\" : \"OpenMount\",\n \"Effect\" : \"Allow\",\n \"Principal\" : {\n \"AWS\" : \"*\"\n },\n \"Action\" : \"s3files:ClientMount\",\n \"Resource\" : \"arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d\"\n } ]\n}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystem_1.json b/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystem_1.json new file mode 100644 index 00000000000..8f61a9660e7 --- /dev/null +++ b/tests/data/placebo/test_s3files_cross_account/s3files.GetFileSystem_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "prefix": "", + "clientToken": "fcaafa80-424f-4547-84f7-2f745a717730", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196", + "tags": [ + { + "key": "Env", + "value": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_cross_account/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_cross_account/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_cross_account/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_query/s3files.GetFileSystem_1.json b/tests/data/placebo/test_s3files_file_system_query/s3files.GetFileSystem_1.json new file mode 100644 index 00000000000..8f61a9660e7 --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_query/s3files.GetFileSystem_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "prefix": "", + "clientToken": "fcaafa80-424f-4547-84f7-2f745a717730", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196", + "tags": [ + { + "key": "Env", + "value": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_query/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_file_system_query/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_query/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_tag/s3files.GetFileSystem_1.json b/tests/data/placebo/test_s3files_file_system_tag/s3files.GetFileSystem_1.json new file mode 100644 index 00000000000..fcfb41fe0c9 --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_tag/s3files.GetFileSystem_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 1, + "minute": 17, + "second": 27, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "prefix": "", + "clientToken": "a1ecf748-523d-46d8-8717-d78baae727f8", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196", + "tags": [ + { + "key": "Env", + "value": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_tag/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_file_system_tag/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..d40a8249773 --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_tag/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 1, + "minute": 17, + "second": 27, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-0a8f3e632ed09c2d4", + "fileSystemId": "fs-0a8f3e632ed09c2d4", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_tag/s3files.ListTagsForResource_1.json b/tests/data/placebo/test_s3files_file_system_tag/s3files.ListTagsForResource_1.json new file mode 100644 index 00000000000..3d743dab55b --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_tag/s3files.ListTagsForResource_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "Env", + "value": "test" + }, + { + "key": "Owner", + "value": "c7n-test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_file_system_tag/tagging.TagResources_1.json b/tests/data/placebo/test_s3files_file_system_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_s3files_file_system_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystemPolicy_1.json b/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystemPolicy_1.json new file mode 100644 index 00000000000..370138200c6 --- /dev/null +++ b/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystemPolicy_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystemId": "fs-02d120d4ae6209b2d", + "policy": "{\n \"Version\" : \"2012-10-17\",\n \"Statement\" : [ {\n \"Sid\" : \"OpenMount\",\n \"Effect\" : \"Allow\",\n \"Principal\" : {\n \"AWS\" : \"*\"\n },\n \"Action\" : \"s3files:ClientMount\",\n \"Resource\" : \"arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d\"\n } ]\n}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystem_1.json b/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystem_1.json new file mode 100644 index 00000000000..8f61a9660e7 --- /dev/null +++ b/tests/data/placebo/test_s3files_has_statement/s3files.GetFileSystem_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "prefix": "", + "clientToken": "fcaafa80-424f-4547-84f7-2f745a717730", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196", + "tags": [ + { + "key": "Env", + "value": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_has_statement/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_has_statement/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_has_statement/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_query/s3files.GetMountTarget_1.json b/tests/data/placebo/test_s3files_mount_target_query/s3files.GetMountTarget_1.json new file mode 100644 index 00000000000..10a98255220 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_query/s3files.GetMountTarget_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "availabilityZoneId": "use1-az6", + "ownerId": "644160558196", + "mountTargetId": "fsmt-0393184a229a68fb1", + "fileSystemId": "fs-02d120d4ae6209b2d", + "subnetId": "subnet-efbcccb7", + "ipv4Address": "172.31.21.134", + "networkInterfaceId": "eni-0e83519e717aa2742", + "vpcId": "vpc-d2d616b5", + "securityGroups": [ + "sg-6c7fa917" + ], + "status": "available" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_query/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_mount_target_query/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_query/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_query/s3files.ListMountTargets_1.json b/tests/data/placebo/test_s3files_mount_target_query/s3files.ListMountTargets_1.json new file mode 100644 index 00000000000..b1192d35472 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_query/s3files.ListMountTargets_1.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "mountTargets": [ + { + "availabilityZoneId": "use1-az6", + "fileSystemId": "fs-02d120d4ae6209b2d", + "ipv4Address": "172.31.21.134", + "status": "available", + "mountTargetId": "fsmt-0393184a229a68fb1", + "networkInterfaceId": "eni-0e83519e717aa2742", + "ownerId": "644160558196", + "subnetId": "subnet-efbcccb7", + "vpcId": "vpc-d2d616b5" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_security_group/ec2.DescribeSecurityGroups_1.json b/tests/data/placebo/test_s3files_mount_target_security_group/ec2.DescribeSecurityGroups_1.json new file mode 100644 index 00000000000..fad6990edc1 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_security_group/ec2.DescribeSecurityGroups_1.json @@ -0,0 +1,190 @@ +{ + "status_code": 200, + "data": { + "SecurityGroups": [ + { + "GroupId": "sg-6c7fa917", + "IpPermissionsEgress": [ + { + "IpProtocol": "-1", + "UserIdGroupPairs": [], + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [] + } + ], + "Tags": [ + { + "Key": "ASV", + "Value": "ASVMCNASTY" + }, + { + "Key": "Stackable", + "Value": "True" + }, + { + "Key": "c7n-public", + "Value": "Policy" + }, + { + "Key": "mcymc", + "Value": "" + }, + { + "Key": "NetworkLocation", + "Value": "Private" + }, + { + "Key": "TestTag", + "Value": "TestValue" + } + ], + "VpcId": "vpc-d2d616b5", + "SecurityGroupArn": "arn:aws:ec2:us-east-1:644160558196:security-group/sg-6c7fa917", + "OwnerId": "644160558196", + "GroupName": "default", + "Description": "default VPC security group", + "IpPermissions": [ + { + "IpProtocol": "tcp", + "FromPort": 0, + "ToPort": 65535, + "UserIdGroupPairs": [ + { + "UserId": "644160558196", + "GroupId": "sg-6c7fa917" + } + ], + "IpRanges": [], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "-1", + "UserIdGroupPairs": [ + { + "UserId": "644160558196", + "GroupId": "sg-6c7fa917" + } + ], + "IpRanges": [ + { + "CidrIp": "108.56.181.242/32" + }, + { + "CidrIp": "67.166.173.46/32" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "tcp", + "FromPort": 22, + "ToPort": 22, + "UserIdGroupPairs": [], + "IpRanges": [ + { + "CidrIp": "0.0.0.0/0" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "tcp", + "FromPort": 8443, + "ToPort": 8443, + "UserIdGroupPairs": [], + "IpRanges": [ + { + "CidrIp": "207.171.167.25/32" + }, + { + "CidrIp": "54.240.217.8/29" + }, + { + "CidrIp": "72.21.196.64/29" + }, + { + "CidrIp": "72.21.198.64/29" + }, + { + "CidrIp": "54.240.217.16/29" + }, + { + "CidrIp": "54.239.98.0/24" + }, + { + "CidrIp": "207.171.167.101/32" + }, + { + "CidrIp": "207.171.167.26/32" + }, + { + "CidrIp": "72.21.217.0/24" + }, + { + "CidrIp": "54.240.217.80/29" + }, + { + "CidrIp": "54.240.217.64/28" + }, + { + "CidrIp": "207.171.172.6/32" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "tcp", + "FromPort": 27017, + "ToPort": 27017, + "UserIdGroupPairs": [], + "IpRanges": [ + { + "CidrIp": "172.31.16.236/32" + } + ], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "udp", + "FromPort": 0, + "ToPort": 65535, + "UserIdGroupPairs": [ + { + "UserId": "644160558196", + "GroupId": "sg-6c7fa917" + } + ], + "IpRanges": [], + "Ipv6Ranges": [], + "PrefixListIds": [] + }, + { + "IpProtocol": "icmp", + "FromPort": -1, + "ToPort": -1, + "UserIdGroupPairs": [ + { + "UserId": "644160558196", + "GroupId": "sg-6c7fa917" + } + ], + "IpRanges": [], + "Ipv6Ranges": [], + "PrefixListIds": [] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_security_group/s3files.GetMountTarget_1.json b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.GetMountTarget_1.json new file mode 100644 index 00000000000..10a98255220 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.GetMountTarget_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "availabilityZoneId": "use1-az6", + "ownerId": "644160558196", + "mountTargetId": "fsmt-0393184a229a68fb1", + "fileSystemId": "fs-02d120d4ae6209b2d", + "subnetId": "subnet-efbcccb7", + "ipv4Address": "172.31.21.134", + "networkInterfaceId": "eni-0e83519e717aa2742", + "vpcId": "vpc-d2d616b5", + "securityGroups": [ + "sg-6c7fa917" + ], + "status": "available" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListMountTargets_1.json b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListMountTargets_1.json new file mode 100644 index 00000000000..b1192d35472 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_security_group/s3files.ListMountTargets_1.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "mountTargets": [ + { + "availabilityZoneId": "use1-az6", + "fileSystemId": "fs-02d120d4ae6209b2d", + "ipv4Address": "172.31.21.134", + "status": "available", + "mountTargetId": "fsmt-0393184a229a68fb1", + "networkInterfaceId": "eni-0e83519e717aa2742", + "ownerId": "644160558196", + "subnetId": "subnet-efbcccb7", + "vpcId": "vpc-d2d616b5" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_subnet/ec2.DescribeSubnets_1.json b/tests/data/placebo/test_s3files_mount_target_subnet/ec2.DescribeSubnets_1.json new file mode 100644 index 00000000000..eda13376961 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_subnet/ec2.DescribeSubnets_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "Subnets": [ + { + "AvailabilityZoneId": "use1-az6", + "MapCustomerOwnedIpOnLaunch": false, + "OwnerId": "644160558196", + "AssignIpv6AddressOnCreation": false, + "Ipv6CidrBlockAssociationSet": [], + "Tags": [ + { + "Key": "Internal", + "Value": "False" + }, + { + "Key": "Name", + "Value": "" + } + ], + "SubnetArn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-efbcccb7", + "EnableDns64": false, + "Ipv6Native": false, + "PrivateDnsNameOptionsOnLaunch": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "BlockPublicAccessStates": { + "InternetGatewayBlockMode": "off" + }, + "SubnetId": "subnet-efbcccb7", + "State": "available", + "VpcId": "vpc-d2d616b5", + "CidrBlock": "172.31.16.0/20", + "AvailableIpAddressCount": 4087, + "AvailabilityZone": "us-east-1b", + "DefaultForAz": true, + "MapPublicIpOnLaunch": false + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_subnet/s3files.GetMountTarget_1.json b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.GetMountTarget_1.json new file mode 100644 index 00000000000..10a98255220 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.GetMountTarget_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "availabilityZoneId": "use1-az6", + "ownerId": "644160558196", + "mountTargetId": "fsmt-0393184a229a68fb1", + "fileSystemId": "fs-02d120d4ae6209b2d", + "subnetId": "subnet-efbcccb7", + "ipv4Address": "172.31.21.134", + "networkInterfaceId": "eni-0e83519e717aa2742", + "vpcId": "vpc-d2d616b5", + "securityGroups": [ + "sg-6c7fa917" + ], + "status": "available" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListMountTargets_1.json b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListMountTargets_1.json new file mode 100644 index 00000000000..b1192d35472 --- /dev/null +++ b/tests/data/placebo/test_s3files_mount_target_subnet/s3files.ListMountTargets_1.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "mountTargets": [ + { + "availabilityZoneId": "use1-az6", + "fileSystemId": "fs-02d120d4ae6209b2d", + "ipv4Address": "172.31.21.134", + "status": "available", + "mountTargetId": "fsmt-0393184a229a68fb1", + "networkInterfaceId": "eni-0e83519e717aa2742", + "ownerId": "644160558196", + "subnetId": "subnet-efbcccb7", + "vpcId": "vpc-d2d616b5" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystemPolicy_1.json b/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystemPolicy_1.json new file mode 100644 index 00000000000..58bcfec02d9 --- /dev/null +++ b/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystemPolicy_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "File system policy does not exist.", + "Code": "ResourceNotFoundException" + }, + "ResponseMetadata": {}, + "errorCode": "ResourceNotFoundException", + "message": "File system policy does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystem_1.json b/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystem_1.json new file mode 100644 index 00000000000..8f61a9660e7 --- /dev/null +++ b/tests/data/placebo/test_s3files_no_policy/s3files.GetFileSystem_1.json @@ -0,0 +1,30 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "prefix": "", + "clientToken": "fcaafa80-424f-4547-84f7-2f745a717730", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196", + "tags": [ + { + "key": "Env", + "value": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3files_no_policy/s3files.ListFileSystems_1.json b/tests/data/placebo/test_s3files_no_policy/s3files.ListFileSystems_1.json new file mode 100644 index 00000000000..58372e862be --- /dev/null +++ b/tests/data/placebo/test_s3files_no_policy/s3files.ListFileSystems_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "fileSystems": [ + { + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 2, + "hour": 23, + "minute": 2, + "second": 38, + "microsecond": 0 + }, + "fileSystemArn": "arn:aws:s3files:us-east-1:644160558196:file-system/fs-02d120d4ae6209b2d", + "fileSystemId": "fs-02d120d4ae6209b2d", + "bucket": "arn:aws:s3:::c7n-s3files-backing-644160558196", + "status": "available", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-s3files-role", + "ownerId": "644160558196" + } + ] + } +} \ No newline at end of file diff --git a/tests/test_policy.py b/tests/test_policy.py index 046bc6eaf59..5d9bcb911cb 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -995,7 +995,7 @@ def test_resource_arn_info(self): 'healthcheck', 'event-rule-target', 'log-metric', 'support-case', 'transit-attachment', 'config-recorder', 'apigw-domain-name', 'backup-job', 'quicksight-account', 'codedeploy-config', - 'cleanrooms-collaboration-member'} + 'cleanrooms-collaboration-member', 's3files-mount-target'} missing_method = [] for k, v in manager.resources.items(): diff --git a/tests/test_s3files.py b/tests/test_s3files.py new file mode 100644 index 00000000000..e908b762399 --- /dev/null +++ b/tests/test_s3files.py @@ -0,0 +1,147 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from .common import BaseTest + + +class S3FilesFileSystemTest(BaseTest): + + def test_s3files_file_system_query(self): + session_factory = self.replay_flight_data('test_s3files_file_system_query') + p = self.load_policy( + {'name': 's3files-fs-query', + 'resource': 'aws.s3files-file-system'}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + fs = resources[0] + self.assertEqual(fs['Tags'], [{'Key': 'Env', 'Value': 'test'}]) + self.assertTrue(fs['bucket']) + self.assertTrue(fs['fileSystemArn'].startswith('arn:aws:s3files:')) + + def test_s3files_cross_account(self): + session_factory = self.replay_flight_data('test_s3files_cross_account') + p = self.load_policy( + {'name': 's3files-cross-account', + 'resource': 'aws.s3files-file-system', + 'filters': [{'type': 'cross-account'}]}, + session_factory=session_factory) + resources = p.run() + # wildcard principal statement in the file system policy + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]['c7n:Policy']) + + def test_s3files_has_statement(self): + session_factory = self.replay_flight_data('test_s3files_has_statement') + p = self.load_policy( + {'name': 's3files-has-statement', + 'resource': 'aws.s3files-file-system', + 'filters': [ + {'type': 'has-statement', + 'statements': [ + {'Effect': 'Allow', + 'Action': 's3files:ClientMount'}]}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_s3files_no_policy(self): + session_factory = self.replay_flight_data('test_s3files_no_policy') + p = self.load_policy( + {'name': 's3files-no-policy', + 'resource': 'aws.s3files-file-system', + 'filters': [{'type': 'cross-account'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 0) + + def test_s3files_file_system_tag(self): + session_factory = self.replay_flight_data('test_s3files_file_system_tag') + p = self.load_policy( + {'name': 's3files-tag', + 'resource': 'aws.s3files-file-system', + 'filters': [{'tag:Owner': 'absent'}], + 'actions': [ + {'type': 'tag', 'key': 'Owner', 'value': 'c7n-test'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + client = session_factory().client('s3files') + tags = client.list_tags_for_resource( + resourceId=resources[0]['fileSystemId'])['tags'] + self.assertIn( + {'key': 'Owner', 'value': 'c7n-test'}, tags) + + +class S3FilesMountTargetTest(BaseTest): + + def test_s3files_mount_target_query(self): + session_factory = self.replay_flight_data('test_s3files_mount_target_query') + p = self.load_policy( + {'name': 's3files-mt-query', + 'resource': 'aws.s3files-mount-target'}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + mt = resources[0] + self.assertTrue(mt['subnetId']) + self.assertTrue(mt['securityGroups']) + + def test_s3files_mount_target_subnet(self): + session_factory = self.replay_flight_data('test_s3files_mount_target_subnet') + p = self.load_policy( + {'name': 's3files-mt-subnet', + 'resource': 'aws.s3files-mount-target', + 'filters': [ + {'type': 'subnet', + 'key': 'State', + 'value': 'available'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_s3files_mount_target_security_group(self): + session_factory = self.replay_flight_data( + 'test_s3files_mount_target_security_group') + p = self.load_policy( + {'name': 's3files-mt-sg', + 'resource': 'aws.s3files-mount-target', + 'filters': [ + {'type': 'security-group', + 'key': 'GroupName', + 'value': 'default'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + + +class S3FilesAccessPointTest(BaseTest): + + def test_s3files_access_point_query(self): + session_factory = self.replay_flight_data('test_s3files_access_point_query') + p = self.load_policy( + {'name': 's3files-ap-query', + 'resource': 'aws.s3files-access-point'}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + ap = resources[0] + self.assertTrue(ap['accessPointArn'].startswith('arn:aws:s3files:')) + self.assertIn('rootDirectory', ap) + self.assertIn('fileSystemId', ap) + self.assertEqual(ap['Tags'], [{'Key': 'Env', 'Value': 'test'}]) + + def test_s3files_access_point_tag(self): + session_factory = self.replay_flight_data('test_s3files_access_point_tag') + p = self.load_policy( + {'name': 's3files-ap-tag', + 'resource': 'aws.s3files-access-point', + 'filters': [{'tag:Owner': 'absent'}], + 'actions': [ + {'type': 'tag', 'key': 'Owner', 'value': 'c7n-test'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + client = session_factory().client('s3files') + tags = client.list_tags_for_resource( + resourceId=resources[0]['accessPointId'])['tags'] + self.assertIn({'key': 'Owner', 'value': 'c7n-test'}, tags) From 46f044b9ca6b8cf00302874c6a1ef9b948d68f93 Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:17:05 -0400 Subject: [PATCH 02/65] aws - add s3tables, table-bucket and table resources (#10973) * aws - add s3tables table-bucket and table resources * Using resource_type arn for tag augment * drop redundant policy_attribute * drop redundant permission_prefix matching service default * add replication filter for table buckets * "use universal tagging for table-bucket and table resources --------- Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> --- c7n/resources/resource_map.py | 2 + c7n/resources/s3tables.py | 278 ++++++++++++++++++ .../s3tables.ListTableBuckets_1.json | 59 ++++ .../s3tables.ListTagsForResource_1.json | 9 + .../tagging.GetResources_1.json | 18 ++ .../tagging.TagResources_1.json | 7 + .../s3tables.GetTableBucketPolicy_1.json | 11 + .../s3tables.GetTableBucketPolicy_2.json | 7 + .../s3tables.GetTableBucketPolicy_3.json | 7 + .../s3tables.ListTableBuckets_1.json | 59 ++++ .../tagging.GetResources_1.json | 36 +++ .../s3tables.GetTableBucketPolicy_1.json | 11 + .../s3tables.GetTableBucketPolicy_2.json | 7 + .../s3tables.GetTableBucketPolicy_3.json | 7 + .../s3tables.ListTableBuckets_1.json | 59 ++++ .../tagging.GetResources_1.json | 36 +++ .../s3tables.ListTableBuckets_1.json | 59 ++++ .../tagging.GetResources_1.json | 36 +++ .../s3tables.ListTableBuckets_1.json | 59 ++++ .../s3tables.ListTagsForResource_1.json | 7 + .../tagging.GetResources_1.json | 36 +++ .../tagging.UntagResources_1.json | 7 + .../s3tables.GetTableBucketReplication_1.json | 11 + .../s3tables.GetTableBucketReplication_2.json | 19 ++ .../s3tables.ListTableBuckets_1.json | 42 +++ .../tagging.GetResources_1.json | 8 + .../s3tables.GetTableBucketReplication_1.json | 11 + .../s3tables.GetTableBucketReplication_2.json | 19 ++ .../s3tables.ListTableBuckets_1.json | 42 +++ .../tagging.GetResources_1.json | 8 + .../s3tables.GetTablePolicy_1.json | 11 + .../s3tables.GetTablePolicy_2.json | 7 + .../s3tables.ListTableBuckets_1.json | 59 ++++ .../s3tables.ListTables_1.json | 7 + .../s3tables.ListTables_2.json | 64 ++++ .../s3tables.ListTables_3.json | 7 + .../tagging.GetResources_1.json | 8 + .../s3tables.ListTableBuckets_1.json | 59 ++++ .../s3tables.ListTables_1.json | 7 + .../s3tables.ListTables_2.json | 64 ++++ .../s3tables.ListTables_3.json | 7 + .../tagging.GetResources_1.json | 8 + tests/test_s3tables.py | 165 +++++++++++ 43 files changed, 1450 insertions(+) create mode 100644 c7n/resources/s3tables.py create mode 100644 tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_add_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_add_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_2.json create mode 100644 tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_3.json create mode 100644 tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_cross_account_org/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_2.json create mode 100644 tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_3.json create mode 100644 tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_has_statement/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_query/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_query/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_2.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_allowed/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_2.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_bucket_replication_foreign/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_1.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_2.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_1.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_2.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_3.json create mode 100644 tests/data/placebo/test_s3_table_cross_account/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_s3_table_query/s3tables.ListTableBuckets_1.json create mode 100644 tests/data/placebo/test_s3_table_query/s3tables.ListTables_1.json create mode 100644 tests/data/placebo/test_s3_table_query/s3tables.ListTables_2.json create mode 100644 tests/data/placebo/test_s3_table_query/s3tables.ListTables_3.json create mode 100644 tests/data/placebo/test_s3_table_query/tagging.GetResources_1.json create mode 100644 tests/test_s3tables.py diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index 5e03b60190a..ce333e478f9 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -305,6 +305,8 @@ "aws.rrset": "c7n.resources.route53.ResourceRecordSet", "aws.s3": "c7n.resources.s3.S3", "aws.s3-directory": "c7n.resources.s3.S3Directory", + "aws.s3-table": "c7n.resources.s3tables.Table", + "aws.s3-table-bucket": "c7n.resources.s3tables.TableBucket", "aws.s3files-access-point": "c7n.resources.s3files.AccessPoint", "aws.s3files-file-system": "c7n.resources.s3files.FileSystem", "aws.s3files-mount-target": "c7n.resources.s3files.MountTarget", diff --git a/c7n/resources/s3tables.py b/c7n/resources/s3tables.py new file mode 100644 index 00000000000..388645e3c61 --- /dev/null +++ b/c7n/resources/s3tables.py @@ -0,0 +1,278 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from c7n.filters import CrossAccountAccessFilter, ListItemFilter +from c7n.filters.policystatement import HasStatementFilter +from c7n.manager import resources +from c7n.query import ( + ChildDescribeSource, + ChildResourceManager, + DescribeWithResourceTags, + QueryResourceManager, + TypeInfo, +) +from c7n.tags import universal_augment +from c7n.utils import local_session, type_schema + + +@resources.register('s3-table-bucket') +class TableBucket(QueryResourceManager): + """AWS S3 Tables - Table Bucket + + https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables.html + """ + + class resource_type(TypeInfo): + service = 's3tables' + enum_spec = ('list_table_buckets', 'tableBuckets', None) + arn = id = 'arn' + name = 'name' + date = 'createdAt' + cfn_type = 'AWS::S3Tables::TableBucket' + universal_taggable = object() + + source_mapping = {'describe': DescribeWithResourceTags} + + +class DescribeTable(ChildDescribeSource): + + def augment(self, resources): + return universal_augment(self.manager, super().augment(resources)) + + +@resources.register('s3-table') +class Table(ChildResourceManager): + """AWS S3 Tables - Table + + https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables.html + """ + + class resource_type(TypeInfo): + service = 's3tables' + parent_spec = ('s3-table-bucket', 'tableBucketARN', True) + enum_spec = ('list_tables', 'tables', None) + arn = id = 'tableARN' + name = 'name' + date = 'createdAt' + cfn_type = 'AWS::S3Tables::Table' + universal_taggable = object() + + source_mapping = {'describe-child': DescribeTable} + + +def _table_namespace(resource): + ns = resource['namespace'] + return ns[0] if isinstance(ns, list) else ns + + +class TableBucketPolicyMixin: + """Annotates table buckets with their resource policy under c7n:Policy.""" + + policy_attribute = 'c7n:Policy' + + def policy_annotate(self, client, resource): + if self.policy_attribute in resource: + return resource + try: + resp = client.get_table_bucket_policy(tableBucketARN=resource['arn']) + resource[self.policy_attribute] = resp.get('resourcePolicy') + except client.exceptions.NotFoundException: + resource[self.policy_attribute] = None + return resource + + +class TablePolicyMixin: + """Annotates tables with their resource policy under c7n:Policy.""" + + policy_attribute = 'c7n:Policy' + + def policy_annotate(self, client, resource): + if self.policy_attribute in resource: + return resource + try: + resp = client.get_table_policy( + tableBucketARN=resource['c7n:parent-id'], + namespace=_table_namespace(resource), + name=resource['name']) + resource[self.policy_attribute] = resp.get('resourcePolicy') + except client.exceptions.NotFoundException: + resource[self.policy_attribute] = None + return resource + + +@TableBucket.filter_registry.register('cross-account') +class TableBucketCrossAccount(TableBucketPolicyMixin, CrossAccountAccessFilter): + """Filter table buckets whose resource policy grants access + outside of allowed accounts or organizations. + + :example: + + .. code-block:: yaml + + policies: + - name: s3-table-bucket-cross-account + resource: aws.s3-table-bucket + filters: + - type: cross-account + whitelist_orgids: + - o-xxxxxxxxxx + """ + permissions = ('s3tables:GetTableBucketPolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3tables') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + +@Table.filter_registry.register('cross-account') +class TableCrossAccount(TablePolicyMixin, CrossAccountAccessFilter): + """Filter tables whose resource policy grants access + outside of allowed accounts or organizations. + + :example: + + .. code-block:: yaml + + policies: + - name: s3-table-cross-account + resource: aws.s3-table + filters: + - type: cross-account + whitelist_orgids: + - o-xxxxxxxxxx + """ + permissions = ('s3tables:GetTablePolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3tables') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + +@TableBucket.filter_registry.register('has-statement') +class TableBucketHasStatement(TableBucketPolicyMixin, HasStatementFilter): + """Find table buckets with matching resource policy statements. + + Table buckets without an attached policy have an empty ``c7n:Policy`` + annotation, so a policy-required control can be expressed by asserting + a mandatory statement is present. + + :example: + + .. code-block:: yaml + + policies: + - name: s3-table-bucket-require-ssl-statement + resource: aws.s3-table-bucket + filters: + - type: has-statement + statements: + - Effect: Deny + Condition: + Bool: + "aws:SecureTransport": "false" + """ + permissions = ('s3tables:GetTableBucketPolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3tables') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + def get_std_format_args(self, bucket): + return { + 'table_bucket_arn': bucket['arn'], + 'account_id': self.manager.config.account_id, + 'region': self.manager.config.region, + } + + +@Table.filter_registry.register('has-statement') +class TableHasStatement(TablePolicyMixin, HasStatementFilter): + """Find tables with matching resource policy statements. + + :example: + + .. code-block:: yaml + + policies: + - name: s3-table-require-ssl-statement + resource: aws.s3-table + filters: + - type: has-statement + statements: + - Effect: Deny + Condition: + Bool: + "aws:SecureTransport": "false" + """ + permissions = ('s3tables:GetTablePolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('s3tables') + resources = [self.policy_annotate(client, r) for r in resources] + return super().process(resources, event) + + def get_std_format_args(self, table): + return { + 'table_arn': table['tableARN'], + 'account_id': self.manager.config.account_id, + 'region': self.manager.config.region, + } + + +@TableBucket.filter_registry.register('replication') +class TableBucketReplication(ListItemFilter): + """Filter table buckets on their replication configuration rules. + + Each destination is annotated with a ``destinationAccount`` derived + from its destination table bucket ARN. Table buckets without a + replication configuration have no rules and only match a ``count: 0`` + filter. + + :example: + + .. code-block:: yaml + + policies: + - name: s3-table-bucket-replicated-outside-org + resource: aws.s3-table-bucket + filters: + - type: replication + attrs: + - type: value + key: destinations[].destinationAccount + op: difference + value: + - "111111111111" + - "222222222222" + """ + schema = type_schema( + 'replication', + attrs={'$ref': '#/definitions/filters_common/list_item_attrs'}, + count={'type': 'number'}, + count_op={'$ref': '#/definitions/filters_common/comparison_operators'}) + permissions = ('s3tables:GetTableBucketReplication',) + annotation_key = 'c7n:Replication' + annotate_items = True + + def process(self, resources, event=None): + self.client = local_session(self.manager.session_factory).client('s3tables') + return super().process(resources, event) + + def get_item_values(self, resource): + if self.annotation_key not in resource: + try: + resource[self.annotation_key] = self.manager.retry( + self.client.get_table_bucket_replication, + tableBucketARN=resource['arn'])['configuration'] + except self.client.exceptions.NotFoundException: + resource[self.annotation_key] = None + if resource[self.annotation_key] is None: + return [] + rules = resource[self.annotation_key]['rules'] + for rule in rules: + for destination in rule['destinations']: + destination['destinationAccount'] = ( + destination['destinationTableBucketARN'].split(':')[4]) + return rules diff --git a/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTagsForResource_1.json b/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTagsForResource_1.json new file mode 100644 index 00000000000..f15a075e532 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_add_tag/s3tables.ListTagsForResource_1.json @@ -0,0 +1,9 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": { + "Env": "test" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..e62cf41f7ff --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.TagResources_1.json b/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_add_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_1.json b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_1.json new file mode 100644 index 00000000000..a657bd0c660 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "The specified bucket policy does not exist.", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "The specified bucket policy does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_2.json b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_2.json new file mode 100644 index 00000000000..e5995227ddc --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "resourcePolicy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"OrgAccess\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3tables:GetTableBucket\",\"s3tables:ListTables\"],\"Resource\":\"arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalOrgID\":\"o-c7ntest4321\"}}}]}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_3.json b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_3.json new file mode 100644 index 00000000000..735530db9f6 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.GetTableBucketPolicy_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "resourcePolicy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"ForeignOrgAccess\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3tables:GetTableBucket\",\"s3tables:ListTables\"],\"Resource\":\"arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalOrgID\":\"o-foreignorg999\"}}}]}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_cross_account_org/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_cross_account_org/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_cross_account_org/tagging.GetResources_1.json new file mode 100644 index 00000000000..9532857557f --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_cross_account_org/tagging.GetResources_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_1.json b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_1.json new file mode 100644 index 00000000000..a657bd0c660 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "The specified bucket policy does not exist.", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "The specified bucket policy does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_2.json b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_2.json new file mode 100644 index 00000000000..e5995227ddc --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "resourcePolicy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"OrgAccess\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3tables:GetTableBucket\",\"s3tables:ListTables\"],\"Resource\":\"arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalOrgID\":\"o-c7ntest4321\"}}}]}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_3.json b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_3.json new file mode 100644 index 00000000000..735530db9f6 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.GetTableBucketPolicy_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "resourcePolicy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"ForeignOrgAccess\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3tables:GetTableBucket\",\"s3tables:ListTables\"],\"Resource\":\"arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalOrgID\":\"o-foreignorg999\"}}}]}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_has_statement/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_has_statement/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_has_statement/tagging.GetResources_1.json new file mode 100644 index 00000000000..9532857557f --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_has_statement/tagging.GetResources_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_query/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_query/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_query/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_query/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_query/tagging.GetResources_1.json new file mode 100644 index 00000000000..9532857557f --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_query/tagging.GetResources_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTagsForResource_1.json b/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTagsForResource_1.json new file mode 100644 index 00000000000..c5db860bca2 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_remove_tag/s3tables.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..9532857557f --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.GetResources_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + }, + { + "ResourceARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "Tags": [ + { + "Key": "Env", + "Value": "test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_remove_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_1.json b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_1.json new file mode 100644 index 00000000000..41a7b2b1b2b --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "The specified replication configuration does not exist.", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "The specified replication configuration does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_2.json b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_2.json new file mode 100644 index 00000000000..5c430179c96 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.GetTableBucketReplication_2.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "versionToken": "77c5f77d21f75ed54446", + "configuration": { + "role": "arn:aws:iam::644160558196:role/c7n-test-s3tables-repl-role", + "rules": [ + { + "destinations": [ + { + "destinationTableBucketARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-dst" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..2062bb5ad0e --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_allowed/s3tables.ListTableBuckets_1.json @@ -0,0 +1,42 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-dst", + "name": "c7n-test-tb-repl-dst", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 16, + "second": 3, + "microsecond": 400266 + }, + "tableBucketId": "d27168d3-0eb2-46c6-b73c-b8803a179b93", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-src", + "name": "c7n-test-tb-repl-src", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 16, + "second": 2, + "microsecond": 367381 + }, + "tableBucketId": "e728dcc6-67be-4db2-8950-a7432ca644c3", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_allowed/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_replication_allowed/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_allowed/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_1.json b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_1.json new file mode 100644 index 00000000000..41a7b2b1b2b --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "The specified replication configuration does not exist.", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "The specified replication configuration does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_2.json b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_2.json new file mode 100644 index 00000000000..5c430179c96 --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.GetTableBucketReplication_2.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "versionToken": "77c5f77d21f75ed54446", + "configuration": { + "role": "arn:aws:iam::644160558196:role/c7n-test-s3tables-repl-role", + "rules": [ + { + "destinations": [ + { + "destinationTableBucketARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-dst" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..2062bb5ad0e --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_foreign/s3tables.ListTableBuckets_1.json @@ -0,0 +1,42 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-dst", + "name": "c7n-test-tb-repl-dst", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 16, + "second": 3, + "microsecond": 400266 + }, + "tableBucketId": "d27168d3-0eb2-46c6-b73c-b8803a179b93", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-repl-src", + "name": "c7n-test-tb-repl-src", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 16, + "second": 2, + "microsecond": 367381 + }, + "tableBucketId": "e728dcc6-67be-4db2-8950-a7432ca644c3", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_bucket_replication_foreign/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_bucket_replication_foreign/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_s3_table_bucket_replication_foreign/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_1.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_1.json new file mode 100644 index 00000000000..a175795ae95 --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "The specified table policy does not exist.", + "Code": "NotFoundException" + }, + "ResponseMetadata": {}, + "message": "The specified table policy does not exist." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_2.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_2.json new file mode 100644 index 00000000000..bd2c8c3a120 --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.GetTablePolicy_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "resourcePolicy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"ForeignOrgTable\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":\"s3tables:GetTableData\",\"Resource\":\"arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant/table/*\",\"Condition\":{\"StringEquals\":{\"aws:PrincipalOrgID\":\"o-foreignorg999\"}}}]}" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_1.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_1.json new file mode 100644 index 00000000000..f3d7733e214 --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_2.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_2.json new file mode 100644 index 00000000000..e809feae3d8 --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_2.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [ + { + "namespace": [ + "testns" + ], + "name": "c7n_table_clean", + "type": "customer", + "tableARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant/table/5a8f296b-646a-4585-8d9d-2493685368b9", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 35, + "microsecond": 91652 + }, + "modifiedAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 35, + "microsecond": 91652 + } + }, + { + "namespace": [ + "testns" + ], + "name": "c7n_table_open", + "type": "customer", + "tableARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant/table/e4602cc0-6907-47d8-abef-12fde3ae9094", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 34, + "microsecond": 687105 + }, + "modifiedAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 34, + "microsecond": 687105 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_3.json b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_3.json new file mode 100644 index 00000000000..f3d7733e214 --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/s3tables.ListTables_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_cross_account/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_cross_account/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_s3_table_cross_account/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_query/s3tables.ListTableBuckets_1.json b/tests/data/placebo/test_s3_table_query/s3tables.ListTableBuckets_1.json new file mode 100644 index 00000000000..0e7921d019c --- /dev/null +++ b/tests/data/placebo/test_s3_table_query/s3tables.ListTableBuckets_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tableBuckets": [ + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-bare", + "name": "c7n-test-tb-bare", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 33, + "microsecond": 817529 + }, + "tableBucketId": "e79fbdd0-7e9d-40b9-8d17-8af57f473a92", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant", + "name": "c7n-test-tb-compliant", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 78992 + }, + "tableBucketId": "7624f277-ff39-4b35-87e1-05e8b6d6c7ef", + "type": "customer" + }, + { + "arn": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-open", + "name": "c7n-test-tb-open", + "ownerAccountId": "644160558196", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 32, + "microsecond": 897519 + }, + "tableBucketId": "5f6ed70b-e674-4589-bc5b-8e3c43f3beb8", + "type": "customer" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_query/s3tables.ListTables_1.json b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_1.json new file mode 100644 index 00000000000..f3d7733e214 --- /dev/null +++ b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_query/s3tables.ListTables_2.json b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_2.json new file mode 100644 index 00000000000..e809feae3d8 --- /dev/null +++ b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_2.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [ + { + "namespace": [ + "testns" + ], + "name": "c7n_table_clean", + "type": "customer", + "tableARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant/table/5a8f296b-646a-4585-8d9d-2493685368b9", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 35, + "microsecond": 91652 + }, + "modifiedAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 35, + "microsecond": 91652 + } + }, + { + "namespace": [ + "testns" + ], + "name": "c7n_table_open", + "type": "customer", + "tableARN": "arn:aws:s3tables:us-east-1:644160558196:bucket/c7n-test-tb-compliant/table/e4602cc0-6907-47d8-abef-12fde3ae9094", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 34, + "microsecond": 687105 + }, + "modifiedAt": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 15, + "hour": 5, + "minute": 15, + "second": 34, + "microsecond": 687105 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_query/s3tables.ListTables_3.json b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_3.json new file mode 100644 index 00000000000..f3d7733e214 --- /dev/null +++ b/tests/data/placebo/test_s3_table_query/s3tables.ListTables_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tables": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_s3_table_query/tagging.GetResources_1.json b/tests/data/placebo/test_s3_table_query/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_s3_table_query/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_s3tables.py b/tests/test_s3tables.py new file mode 100644 index 00000000000..c10a314a816 --- /dev/null +++ b/tests/test_s3tables.py @@ -0,0 +1,165 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from .common import BaseTest + + +class S3TableBucketTest(BaseTest): + + def test_s3_table_bucket_query(self): + session_factory = self.replay_flight_data('test_s3_table_bucket_query') + p = self.load_policy( + {'name': 's3-table-bucket-query', + 'resource': 'aws.s3-table-bucket'}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 3) + tagged = {r['name']: r['Tags'] for r in resources} + self.assertEqual( + tagged['c7n-test-tb-compliant'], + [{'Key': 'Env', 'Value': 'test'}]) + + def test_s3_table_bucket_cross_account_org(self): + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_cross_account_org') + p = self.load_policy( + {'name': 's3-table-bucket-cross-account', + 'resource': 'aws.s3-table-bucket', + 'filters': [ + {'type': 'cross-account', + 'whitelist_orgids': ['o-c7ntest4321']}]}, + session_factory=session_factory) + resources = p.run() + # the whitelisted-org bucket passes, the no-policy bucket is + # skipped, only the foreign-org bucket is flagged + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]['name'], 'c7n-test-tb-open') + self.assertTrue(resources[0]['c7n:Policy']) + + def test_s3_table_bucket_has_statement(self): + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_has_statement') + p = self.load_policy( + {'name': 's3-table-bucket-has-statement', + 'resource': 'aws.s3-table-bucket', + 'filters': [ + {'type': 'has-statement', + 'statements': [ + {'Effect': 'Allow', + 'Condition': { + 'StringEquals': { + 'aws:PrincipalOrgID': 'o-c7ntest4321'}}}]}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]['name'], 'c7n-test-tb-compliant') + + def test_s3_table_bucket_add_tag(self): + session_factory = self.replay_flight_data('test_s3_table_bucket_add_tag') + p = self.load_policy( + {'name': 's3-table-bucket-tag', + 'resource': 'aws.s3-table-bucket', + 'filters': [{'tag:Env': 'absent'}], + 'actions': [ + {'type': 'tag', 'key': 'Env', 'value': 'test'}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 2) + client = session_factory().client('s3tables') + tags = client.list_tags_for_resource( + resourceArn=resources[0]['arn'])['tags'] + self.assertEqual(tags.get('Env'), 'test') + + def test_s3_table_bucket_remove_tag(self): + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_remove_tag') + p = self.load_policy( + {'name': 's3-table-bucket-remove-tag', + 'resource': 'aws.s3-table-bucket', + 'filters': [{'tag:Env': 'present'}], + 'actions': [{'type': 'remove-tag', 'tags': ['Env']}]}, + session_factory=session_factory) + resources = p.run() + self.assertTrue(resources) + client = session_factory().client('s3tables') + tags = client.list_tags_for_resource( + resourceArn=resources[0]['arn'])['tags'] + self.assertNotIn('Env', tags) + + def test_s3_table_bucket_replication_foreign(self): + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_replication_foreign') + p = self.load_policy( + {'name': 's3-table-bucket-replication-foreign', + 'resource': 'aws.s3-table-bucket', + 'filters': [ + {'type': 'replication', + 'attrs': [ + {'type': 'value', + 'key': 'destinations[].destinationAccount', + 'op': 'difference', + 'value': ['111111111111']}]}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]['name'], 'c7n-test-tb-repl-src') + matched = resources[0]['c7n:ListItemMatches'] + self.assertEqual( + matched[0]['destinations'][0]['destinationAccount'], + '644160558196') + + def test_s3_table_bucket_replication_allowed(self): + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_replication_allowed') + p = self.load_policy( + {'name': 's3-table-bucket-replication-allowed', + 'resource': 'aws.s3-table-bucket', + 'filters': [ + {'type': 'replication', + 'attrs': [ + {'type': 'value', + 'key': 'destinations[].destinationAccount', + 'op': 'difference', + 'value': ['644160558196']}]}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 0) + + def test_s3_table_bucket_replication_count(self): + # unreplicated buckets match count: 0 + session_factory = self.replay_flight_data( + 'test_s3_table_bucket_replication_allowed') + p = self.load_policy( + {'name': 's3-table-bucket-not-replicated', + 'resource': 'aws.s3-table-bucket', + 'filters': [{'type': 'replication', 'count': 0}]}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]['name'], 'c7n-test-tb-repl-dst') + + +class S3TableTest(BaseTest): + + def test_s3_table_query(self): + session_factory = self.replay_flight_data('test_s3_table_query') + p = self.load_policy( + {'name': 's3-table-query', + 'resource': 'aws.s3-table'}, + session_factory=session_factory) + resources = p.run() + self.assertEqual(len(resources), 2) + for r in resources: + self.assertTrue(r['c7n:parent-id'].startswith('arn:aws:s3tables:')) + + def test_s3_table_cross_account(self): + session_factory = self.replay_flight_data('test_s3_table_cross_account') + p = self.load_policy( + {'name': 's3-table-cross-account', + 'resource': 'aws.s3-table', + 'filters': [{'type': 'cross-account'}]}, + session_factory=session_factory) + resources = p.run() + # only the foreign-org table policy is flagged; the + # policy-less table is skipped + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]['name'], 'c7n_table_open') From 6cd074b2c8a39124ff6dd3946d62a9f0a8dab7bb Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:05:16 -0400 Subject: [PATCH 03/65] aws - eventbridge - adding event-connection resource (#11007) * adding event-connection resource * Update test_policy.py --------- Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> --- c7n/resources/eventbridge.py | 31 +++++++ c7n/resources/resource_map.py | 1 + .../events.DescribeConnection_1.json | 46 +++++++++++ .../events.DescribeConnection_2.json | 46 +++++++++++ .../events.ListConnections_1.json | 80 +++++++++++++++++++ tests/test_eventbridge.py | 42 ++++++++++ tests/test_policy.py | 1 - 7 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tests/data/placebo/test_event_connection_query/events.DescribeConnection_1.json create mode 100644 tests/data/placebo/test_event_connection_query/events.DescribeConnection_2.json create mode 100644 tests/data/placebo/test_event_connection_query/events.ListConnections_1.json diff --git a/c7n/resources/eventbridge.py b/c7n/resources/eventbridge.py index c6aa4446301..becc18d6d9d 100644 --- a/c7n/resources/eventbridge.py +++ b/c7n/resources/eventbridge.py @@ -106,6 +106,37 @@ class resource_type(TypeInfo): id = name = 'Name' +@resources.register('event-connection') +class EventConnection(QueryResourceManager): + """EventBridge Connection Resource + + :example: + + .. code-block:: yaml + + policies: + - name: eventbridge-connection-basic-auth + resource: aws.event-connection + filters: + - type: value + key: AuthorizationType + op: not-in + value: + - OAUTH_CLIENT_CREDENTIALS + - API_KEY + """ + + class resource_type(TypeInfo): + service = 'events' + arn_type = 'connection' + arn = 'ConnectionArn' + enum_spec = ('list_connections', 'Connections', None) + detail_spec = ('describe_connection', 'Name', 'Name', None) + config_type = cfn_type = 'AWS::Events::Connection' + id = name = 'Name' + date = 'LastModifiedTime' + + class EventRuleQuery(ChildResourceQuery): def get_parent_parameters(self, params, parent_id, parent_key): diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index ce333e478f9..05094b96978 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -158,6 +158,7 @@ "aws.eni": "c7n.resources.vpc.NetworkInterface", "aws.event-api-destination": "c7n.resources.eventbridge.EventApiDestination", "aws.event-bus": "c7n.resources.eventbridge.EventBus", + "aws.event-connection": "c7n.resources.eventbridge.EventConnection", "aws.event-rule": "c7n.resources.eventbridge.EventRule", "aws.event-rule-target": "c7n.resources.eventbridge.EventRuleTarget", "aws.eventbridge-pipes": "c7n.resources.eventbridge.EventBridgePipes", diff --git a/tests/data/placebo/test_event_connection_query/events.DescribeConnection_1.json b/tests/data/placebo/test_event_connection_query/events.DescribeConnection_1.json new file mode 100644 index 00000000000..9e7bfc25261 --- /dev/null +++ b/tests/data/placebo/test_event_connection_query/events.DescribeConnection_1.json @@ -0,0 +1,46 @@ +{ + "status_code": 200, + "data": { + "ConnectionArn": "arn:aws:events:us-east-1:644160558196:connection/c7n-test-conn-apikey/6e9c4e5e-0202-4faa-9cab-084328b76102", + "Name": "c7n-test-conn-apikey", + "ConnectionState": "AUTHORIZED", + "AuthorizationType": "API_KEY", + "SecretArn": "arn:aws:secretsmanager:us-east-1:644160558196:secret:events!connection/c7n-test-conn-apikey/b34fa77f-5c25-4b80-af6f-76153a18ddaf-o0S3lh", + "AuthParameters": { + "ApiKeyAuthParameters": { + "ApiKeyName": "x-api-key" + } + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + }, + "LastAuthorizedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_event_connection_query/events.DescribeConnection_2.json b/tests/data/placebo/test_event_connection_query/events.DescribeConnection_2.json new file mode 100644 index 00000000000..6ed1bf7ec06 --- /dev/null +++ b/tests/data/placebo/test_event_connection_query/events.DescribeConnection_2.json @@ -0,0 +1,46 @@ +{ + "status_code": 200, + "data": { + "ConnectionArn": "arn:aws:events:us-east-1:644160558196:connection/c7n-test-conn-basic/95cc4260-854b-44b1-9f02-582b00e0be35", + "Name": "c7n-test-conn-basic", + "ConnectionState": "AUTHORIZED", + "AuthorizationType": "BASIC", + "SecretArn": "arn:aws:secretsmanager:us-east-1:644160558196:secret:events!connection/c7n-test-conn-basic/a0ccaa8e-bc55-43a7-9296-c200d0afcf86-LSS8wK", + "AuthParameters": { + "BasicAuthParameters": { + "Username": "c7ntest" + } + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + }, + "LastAuthorizedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_event_connection_query/events.ListConnections_1.json b/tests/data/placebo/test_event_connection_query/events.ListConnections_1.json new file mode 100644 index 00000000000..6fddfcf65ad --- /dev/null +++ b/tests/data/placebo/test_event_connection_query/events.ListConnections_1.json @@ -0,0 +1,80 @@ +{ + "status_code": 200, + "data": { + "Connections": [ + { + "ConnectionArn": "arn:aws:events:us-east-1:644160558196:connection/c7n-test-conn-apikey/6e9c4e5e-0202-4faa-9cab-084328b76102", + "Name": "c7n-test-conn-apikey", + "ConnectionState": "AUTHORIZED", + "AuthorizationType": "API_KEY", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + }, + "LastAuthorizedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 29, + "microsecond": 0 + } + }, + { + "ConnectionArn": "arn:aws:events:us-east-1:644160558196:connection/c7n-test-conn-basic/95cc4260-854b-44b1-9f02-582b00e0be35", + "Name": "c7n-test-conn-basic", + "ConnectionState": "AUTHORIZED", + "AuthorizationType": "BASIC", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + }, + "LastAuthorizedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 13, + "hour": 2, + "minute": 29, + "second": 28, + "microsecond": 0 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_eventbridge.py b/tests/test_eventbridge.py index 369bfd03f35..eeddd8594d5 100644 --- a/tests/test_eventbridge.py +++ b/tests/test_eventbridge.py @@ -384,3 +384,45 @@ def test_api_destination_query(self): self.assertEqual(len(resources), 1) self.assertEqual(resources[0]["Name"], "test-delete-me") self.assertEqual(resources[0]["ApiDestinationState"], "ACTIVE") + + +class ConnectionTest(BaseTest): + + def test_event_connection_query(self): + factory = self.replay_flight_data("test_event_connection_query") + p = self.load_policy( + { + "name": "query-connections", + "resource": "aws.event-connection", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 2) + self.assertEqual( + sorted(r["AuthorizationType"] for r in resources), + ["API_KEY", "BASIC"]) + self.assertTrue(resources[0]["ConnectionArn"].startswith( + "arn:aws:events:us-east-1:644160558196:connection/")) + + def test_event_connection_basic_auth_filter(self): + factory = self.replay_flight_data("test_event_connection_query") + p = self.load_policy( + { + "name": "eventbridge-connection-basic-auth", + "resource": "aws.event-connection", + "filters": [ + { + "type": "value", + "key": "AuthorizationType", + "op": "not-in", + "value": ["OAUTH_CLIENT_CREDENTIALS", "API_KEY"], + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["AuthorizationType"], "BASIC") + self.assertEqual(resources[0]["Name"], "c7n-test-conn-basic") diff --git a/tests/test_policy.py b/tests/test_policy.py index 5d9bcb911cb..b206b4ed7bb 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -656,7 +656,6 @@ def test_config_resource_support(self): 'AWS::EventSchemas::RegistryPolicy', 'AWS::EventSchemas::Schema', 'AWS::Events::Archive', - 'AWS::Events::Connection', 'AWS::Events::Endpoint', 'AWS::FraudDetector::EntityType', 'AWS::FraudDetector::Label', From 73c8e1537d720abc510ac8cfa0856d7dd37e9dad Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:42:16 -0400 Subject: [PATCH 04/65] aws - ssm - add tagging support for managed instances, ops items, patch baselines, maintenance windows, and associations (#11093) * ssm - add tagging support for managed instances, ops items, patch baselines, maintenance windows, and associations * Update ssm.py --- c7n/resources/resource_map.py | 3 + c7n/resources/ssm.py | 296 +++++++++++++++++- .../ssm.ListTagsForResource_1.json | 7 + .../ssm.AddTagsToResource_1.json | 6 + .../ssm.DescribeOpsItems_1.json | 66 ++++ .../ssm.ListTagsForResource_1.json | 7 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.AddTagsToResource_1.json | 6 + .../ssm.DescribeOpsItems_1.json | 66 ++++ .../ssm.DescribeOpsItems_2.json | 66 ++++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.ListTagsForResource_4.json | 12 + .../ssm.ListTagsForResource_5.json | 12 + .../ssm.ListTagsForResource_6.json | 7 + .../ssm.RemoveTagsFromResource_1.json | 6 + .../ssm.ListAssociations_1.json | 44 +++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.AddTagsToResource_1.json | 6 + .../ssm.ListAssociations_1.json | 44 +++ .../ssm.ListAssociations_2.json | 44 +++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.ListTagsForResource_4.json | 12 + .../ssm.ListTagsForResource_5.json | 12 + .../ssm.ListTagsForResource_6.json | 7 + .../ssm.RemoveTagsFromResource_1.json | 6 + .../ssm.ListTagsForResource_1.json | 7 + .../ssm.DescribeMaintenanceWindows_1.json | 26 ++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.AddTagsToResource_1.json | 6 + .../ssm.DescribeMaintenanceWindows_1.json | 26 ++ .../ssm.DescribeMaintenanceWindows_2.json | 26 ++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.ListTagsForResource_4.json | 12 + .../ssm.ListTagsForResource_5.json | 12 + .../ssm.ListTagsForResource_6.json | 7 + .../ssm.RemoveTagsFromResource_1.json | 6 + .../ec2.DescribeTags_1.json | 20 ++ .../ec2.DescribeTags_2.json | 20 ++ .../ssm.AddTagsToResource_1.json | 6 + .../ssm.DescribeInstanceInformation_1.json | 120 +++++++ .../ssm.DescribeInstanceInformation_2.json | 120 +++++++ .../ssm.ListTagsForResource_1.json | 7 + .../ssm.ListTagsForResource_2.json | 12 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.ListTagsForResource_4.json | 7 + .../ssm.RemoveTagsFromResource_1.json | 6 + .../ssm.DescribePatchBaselines_1.json | 20 ++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.AddTagsToResource_1.json | 6 + .../ssm.DescribePatchBaselines_1.json | 20 ++ .../ssm.DescribePatchBaselines_2.json | 20 ++ .../ssm.ListTagsForResource_1.json | 12 + .../ssm.ListTagsForResource_2.json | 7 + .../ssm.ListTagsForResource_3.json | 12 + .../ssm.ListTagsForResource_4.json | 12 + .../ssm.ListTagsForResource_5.json | 12 + .../ssm.ListTagsForResource_6.json | 7 + .../ssm.RemoveTagsFromResource_1.json | 6 + .../ec2.DescribeTags_1.json | 7 + .../ssm.ListTagsForResource_1.json | 7 + tests/test_ssm.py | 295 +++++++++++++++++ 71 files changed, 1802 insertions(+), 3 deletions(-) create mode 100644 tests/data/placebo/test_ops_item_get_resources/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ops_item_mark_for_op/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ops_item_mark_for_op/ssm.DescribeOpsItems_1.json create mode 100644 tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_1.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_2.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_5.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_6.json create mode 100644 tests/data/placebo/test_ops_item_tag/ssm.RemoveTagsFromResource_1.json create mode 100644 tests/data/placebo/test_ssm_association_query/ssm.ListAssociations_1.json create mode 100644 tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_1.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_2.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_5.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_6.json create mode 100644 tests/data/placebo/test_ssm_association_tag/ssm.RemoveTagsFromResource_1.json create mode 100644 tests/data/placebo/test_ssm_get_managed_instances/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_query/ssm.DescribeMaintenanceWindows_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_2.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_5.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_6.json create mode 100644 tests/data/placebo/test_ssm_maintenance_window_tag/ssm.RemoveTagsFromResource_1.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_1.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_2.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_1.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_2.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_ssm_managed_instance_tag/ssm.RemoveTagsFromResource_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_query/ssm.DescribePatchBaselines_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.AddTagsToResource_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_2.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_5.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_6.json create mode 100644 tests/data/placebo/test_ssm_patch_baseline_tag/ssm.RemoveTagsFromResource_1.json create mode 100644 tests/data/placebo/test_ssm_send_command/ec2.DescribeTags_1.json create mode 100644 tests/data/placebo/test_update_ops_item/ssm.ListTagsForResource_1.json diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index 05094b96978..08873841253 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -367,10 +367,13 @@ "aws.sns-subscription": "c7n.resources.sns.SNSSubscription", "aws.sqs": "c7n.resources.sqs.SQS", "aws.ssm-activation": "c7n.resources.ssm.SSMActivation", + "aws.ssm-association": "c7n.resources.ssm.SSMAssociation", "aws.ssm-data-sync": "c7n.resources.ssm.SSMDataSync", "aws.ssm-document": "c7n.resources.ssm.SSMDocument", + "aws.ssm-maintenance-window": "c7n.resources.ssm.SSMMaintenanceWindow", "aws.ssm-managed-instance": "c7n.resources.ssm.ManagedInstance", "aws.ssm-parameter": "c7n.resources.ssm.SSMParameter", + "aws.ssm-patch-baseline": "c7n.resources.ssm.SSMPatchBaseline", "aws.ssm-patch-group": "c7n.resources.ssm.SsmPatchGroup", "aws.ssm-session-manager": "c7n.resources.ssm.SSMSessionManager", "aws.step-machine": "c7n.resources.sfn.StepFunction", diff --git a/c7n/resources/ssm.py b/c7n/resources/ssm.py index d53505a7c4c..ddc817eea7a 100644 --- a/c7n/resources/ssm.py +++ b/c7n/resources/ssm.py @@ -9,9 +9,15 @@ from c7n.exceptions import PolicyValidationError from c7n.filters import Filter, CrossAccountAccessFilter, ValueFilter from c7n.filters.kms import KmsRelatedFilter -from c7n.query import QueryResourceManager, TypeInfo +from c7n.query import DescribeSource, QueryResourceManager, TypeInfo from c7n.manager import resources -from c7n.tags import universal_augment +from c7n.tags import ( + universal_augment, + Tag as TagAction, + RemoveTag as RemoveTagAction, + TagDelayedAction, + TagActionFilter, +) from c7n.utils import chunks, get_retry, local_session, type_schema, filter_empty from c7n.version import version @@ -52,8 +58,155 @@ def process(self, resources): ignore_err_codes=('ParameterNotFound',)) +def _hybrid_instances(resources): + # ssm tag apis only support hybrid (mi-*) managed instances + return [r for r in resources if r['InstanceId'].startswith('mi-')] + + +class DescribeSSMTags(DescribeSource): + """Augment ssm resources with tags via ssm:ListTagsForResource. + + ssm's tag apis are keyed on ResourceType + ResourceId rather than + arns, and the resource groups tagging api doesn't support these + resource types (only ssm parameters), so universal (rgta) tagging + can't be used here. + + The manager class must define `tag_resource_type` with the resource's + ssm AddTagsToResource/ListTagsForResource ResourceType value. + """ + + def get_taggable(self, resources): + return resources + + def augment(self, resources): + resources = super().augment(resources) + mid = self.manager.resource_type.id + rtype = self.manager.tag_resource_type + + def _fetch_tags(resource_set): + client = local_session(self.manager.session_factory).client('ssm') + for r in resource_set: + r['Tags'] = self.manager.retry( + client.list_tags_for_resource, + ResourceType=rtype, + ResourceId=r[mid]).get('TagList', []) + + with self.manager.executor_factory(max_workers=2) as w: + list(w.map(_fetch_tags, chunks(self.get_taggable(resources), 20))) + return resources + + +class DescribeManagedInstance(DescribeSSMTags): + # ec2 (i-*) instance tags aren't reachable via the ssm tag apis + def get_taggable(self, resources): + return _hybrid_instances(resources) + + def augment(self, resources): + resources = super().augment(resources) + ec2_instances = [ + r for r in resources if not r['InstanceId'].startswith('mi-')] + if not ec2_instances: + return resources + client = local_session(self.manager.session_factory).client('ec2') + tag_map = {} + for chunk in chunks(ec2_instances, 20): + for t in self.manager.retry( + client.describe_tags, + Filters=[{ + 'Name': 'resource-id', + 'Values': [r['InstanceId'] for r in chunk]}]).get( + 'Tags', ()): + tag_map.setdefault(t['ResourceId'], []).append( + {'Key': t['Key'], 'Value': t['Value']}) + for r in ec2_instances: + r['Tags'] = tag_map.get(r['InstanceId'], []) + return resources + + +class TagSSMResource(TagAction): + """Tag an ssm resource via ssm:AddTagsToResource.""" + + permissions = ('ssm:AddTagsToResource',) + + def get_taggable(self, resources): + return resources + + def process_resource_set(self, client, resource_set, tags): + mid = self.manager.get_model().id + for r in self.get_taggable(resource_set): + self.manager.retry( + client.add_tags_to_resource, + ResourceType=self.manager.tag_resource_type, + ResourceId=r[mid], + Tags=tags) + + +class RemoveTagSSMResource(RemoveTagAction): + """Remove tags from an ssm resource via ssm:RemoveTagsFromResource.""" + + permissions = ('ssm:RemoveTagsFromResource',) + + def get_taggable(self, resources): + return resources + + def process_resource_set(self, client, resource_set, tag_keys): + mid = self.manager.get_model().id + for r in self.get_taggable(resource_set): + self.manager.retry( + client.remove_tags_from_resource, + ResourceType=self.manager.tag_resource_type, + ResourceId=r[mid], + TagKeys=tag_keys) + + +class _ManagedInstanceOnly: + + def get_taggable(self, resources): + hybrid = _hybrid_instances(resources) + if len(hybrid) != len(resources): + self.log.warning( + "ssm tag apis only support hybrid (mi-*) managed instances, " + "skipping %d ec2 instances; use the ec2 resource to tag those", + len(resources) - len(hybrid)) + return hybrid + + +class TagManagedInstance(_ManagedInstanceOnly, TagSSMResource): + pass + + +class RemoveTagManagedInstance(_ManagedInstanceOnly, RemoveTagSSMResource): + pass + + @resources.register('ssm-managed-instance') class ManagedInstance(QueryResourceManager): + """Instances managed by ssm, both ec2 (i-*) and hybrid (mi-*) instances. + + Tags are fetched via the ssm tag apis for hybrid instances and from + ec2 for ec2 instances, so tag filters work across the whole resource + population. The tag actions only apply to hybrid instances, as the + ssm tag apis don't support ec2 instances; use the `aws.ec2` resource + to manage those tags. + + :example: + + .. code-block:: yaml + + policies: + - name: ssm-hybrid-instance-untagged + resource: aws.ssm-managed-instance + filters: + - type: value + key: InstanceId + op: glob + value: "mi-*" + - "tag:Owner": absent + actions: + - type: tag + key: Owner + value: unknown + """ class resource_type(TypeInfo): service = 'ssm' @@ -62,8 +215,16 @@ class resource_type(TypeInfo): name = 'Name' date = 'RegistrationDate' arn_type = "managed-instance" + permissions_augment = ("ssm:ListTagsForResource", "ec2:DescribeTags") + + tag_resource_type = 'ManagedInstance' + source_mapping = {'describe': DescribeManagedInstance} + - permissions = ('ssm:DescribeInstanceInformation',) +ManagedInstance.action_registry.register('tag', TagManagedInstance) +ManagedInstance.action_registry.register('remove-tag', RemoveTagManagedInstance) +ManagedInstance.action_registry.register('mark-for-op', TagDelayedAction) +ManagedInstance.filter_registry.register('marked-for-op', TagActionFilter) @EC2.action_registry.register('send-command') @@ -177,11 +338,15 @@ class resource_type(TypeInfo): arn_type = 'opsitem' id = 'OpsItemId' name = 'Title' + permissions_augment = ("ssm:ListTagsForResource",) default_report_fields = ( 'Status', 'Title', 'LastModifiedTime', 'CreatedBy', 'CreatedTime') + tag_resource_type = 'OpsItem' + source_mapping = {'describe': DescribeSSMTags} + QueryKeys = { 'Status', 'CreatedBy', @@ -230,6 +395,12 @@ def resource_query(self): return {'OpsItemFilters': filters} +OpsItem.action_registry.register('tag', TagSSMResource) +OpsItem.action_registry.register('remove-tag', RemoveTagSSMResource) +OpsItem.action_registry.register('mark-for-op', TagDelayedAction) +OpsItem.filter_registry.register('marked-for-op', TagActionFilter) + + @OpsItem.action_registry.register('update') class UpdateOpsItem(Action): """Update an ops item. @@ -885,6 +1056,125 @@ class resource_type(TypeInfo): name = "PatchGroup" +@resources.register('ssm-patch-baseline') +class SSMPatchBaseline(QueryResourceManager): + """Custom (account-owned) ssm patch baselines. + + AWS predefined baselines are owned by an aws account and can't be + tagged or modified, so only self-owned baselines are enumerated. + + :example: + + .. code-block:: yaml + + policies: + - name: ssm-patch-baseline-untagged + resource: aws.ssm-patch-baseline + filters: + - "tag:Owner": absent + actions: + - type: tag + key: Owner + value: unknown + """ + + class resource_type(TypeInfo): + service = 'ssm' + enum_spec = ('describe_patch_baselines', 'BaselineIdentities', { + 'Filters': [{'Key': 'OWNER', 'Values': ['Self']}]}) + id = 'BaselineId' + name = 'BaselineName' + arn_type = 'patchbaseline' + cfn_type = 'AWS::SSM::PatchBaseline' + permissions_augment = ("ssm:ListTagsForResource",) + + tag_resource_type = 'PatchBaseline' + source_mapping = {'describe': DescribeSSMTags} + + +SSMPatchBaseline.action_registry.register('tag', TagSSMResource) +SSMPatchBaseline.action_registry.register('remove-tag', RemoveTagSSMResource) +SSMPatchBaseline.action_registry.register('mark-for-op', TagDelayedAction) +SSMPatchBaseline.filter_registry.register('marked-for-op', TagActionFilter) + + +@resources.register('ssm-maintenance-window') +class SSMMaintenanceWindow(QueryResourceManager): + """SSM maintenance windows. + + :example: + + .. code-block:: yaml + + policies: + - name: ssm-maintenance-window-untagged + resource: aws.ssm-maintenance-window + filters: + - "tag:Owner": absent + actions: + - type: tag + key: Owner + value: unknown + """ + + class resource_type(TypeInfo): + service = 'ssm' + enum_spec = ('describe_maintenance_windows', 'WindowIdentities', None) + id = 'WindowId' + name = 'Name' + arn_type = 'maintenancewindow' + cfn_type = 'AWS::SSM::MaintenanceWindow' + permissions_augment = ("ssm:ListTagsForResource",) + + tag_resource_type = 'MaintenanceWindow' + source_mapping = {'describe': DescribeSSMTags} + + +SSMMaintenanceWindow.action_registry.register('tag', TagSSMResource) +SSMMaintenanceWindow.action_registry.register('remove-tag', RemoveTagSSMResource) +SSMMaintenanceWindow.action_registry.register('mark-for-op', TagDelayedAction) +SSMMaintenanceWindow.filter_registry.register('marked-for-op', TagActionFilter) + + +@resources.register('ssm-association') +class SSMAssociation(QueryResourceManager): + """SSM State Manager associations. + + :example: + + .. code-block:: yaml + + policies: + - name: ssm-association-untagged + resource: aws.ssm-association + filters: + - "tag:Owner": absent + actions: + - type: tag + key: Owner + value: unknown + """ + + class resource_type(TypeInfo): + service = 'ssm' + enum_spec = ('list_associations', 'Associations', None) + id = 'AssociationId' + name = 'AssociationName' + date = 'LastExecutionDate' + arn_type = 'association' + cfn_type = 'AWS::SSM::Association' + permissions_augment = ("ssm:ListTagsForResource",) + + tag_resource_type = 'Association' + source_mapping = {'describe': DescribeSSMTags} + + +SSMAssociation.action_registry.register('tag', TagSSMResource) +SSMAssociation.action_registry.register('remove-tag', RemoveTagSSMResource) +SSMAssociation.action_registry.register('mark-for-op', TagDelayedAction) +SSMAssociation.filter_registry.register('marked-for-op', TagActionFilter) + + @resources.register('ssm-session-manager') class SSMSessionManager(QueryResourceManager): diff --git a/tests/data/placebo/test_ops_item_get_resources/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ops_item_get_resources/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ops_item_get_resources/ssm.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_mark_for_op/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ops_item_mark_for_op/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ops_item_mark_for_op/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ops_item_mark_for_op/ssm.DescribeOpsItems_1.json b/tests/data/placebo/test_ops_item_mark_for_op/ssm.DescribeOpsItems_1.json new file mode 100644 index 00000000000..acd3e05a79a --- /dev/null +++ b/tests/data/placebo/test_ops_item_mark_for_op/ssm.DescribeOpsItems_1.json @@ -0,0 +1,66 @@ +{ + "status_code": 200, + "data": { + "OpsItemSummaries": [ + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-5576c67d5c46", + "Title": "c7n-test-mark", + "OpsItemType": "/aws/issue" + }, + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-82ad691908f6", + "Title": "c7n-test-tag", + "OpsItemType": "/aws/issue" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..4a18d9ec593 --- /dev/null +++ b/tests/data/placebo/test_ops_item_mark_for_op/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "custodian_cleanup", + "Value": "Resource does not meet policy: update@2026/08/21" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ops_item_tag/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_1.json b/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_1.json new file mode 100644 index 00000000000..acd3e05a79a --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_1.json @@ -0,0 +1,66 @@ +{ + "status_code": 200, + "data": { + "OpsItemSummaries": [ + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-5576c67d5c46", + "Title": "c7n-test-mark", + "OpsItemType": "/aws/issue" + }, + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-82ad691908f6", + "Title": "c7n-test-tag", + "OpsItemType": "/aws/issue" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_2.json b/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_2.json new file mode 100644 index 00000000000..acd3e05a79a --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.DescribeOpsItems_2.json @@ -0,0 +1,66 @@ +{ + "status_code": 200, + "data": { + "OpsItemSummaries": [ + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 51, + "microsecond": 311000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-5576c67d5c46", + "Title": "c7n-test-mark", + "OpsItemType": "/aws/issue" + }, + { + "CreatedBy": "arn:aws:iam::644160558196:user/developer", + "CreatedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "LastModifiedBy": "arn:aws:iam::644160558196:user/developer", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 18, + "minute": 27, + "second": 50, + "microsecond": 482000 + }, + "Source": "c7n-test", + "Status": "Open", + "OpsItemId": "oi-82ad691908f6", + "Title": "c7n-test-tag", + "OpsItemType": "/aws/issue" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..3b3b34ff07d --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "maid_status", + "Value": "Resource does not meet policy: update@2026/08/21" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..2ad1208417a --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_4.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_4.json new file mode 100644 index 00000000000..3b3b34ff07d --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_4.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "maid_status", + "Value": "Resource does not meet policy: update@2026/08/21" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_5.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_5.json new file mode 100644 index 00000000000..2ad1208417a --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_5.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_6.json b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_6.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.ListTagsForResource_6.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ops_item_tag/ssm.RemoveTagsFromResource_1.json b/tests/data/placebo/test_ops_item_tag/ssm.RemoveTagsFromResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ops_item_tag/ssm.RemoveTagsFromResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_association_query/ssm.ListAssociations_1.json b/tests/data/placebo/test_ssm_association_query/ssm.ListAssociations_1.json new file mode 100644 index 00000000000..d6f28eed1ac --- /dev/null +++ b/tests/data/placebo/test_ssm_association_query/ssm.ListAssociations_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "Associations": [ + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "b1f5c622-1777-4264-9518-429dbe6fd231", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-tagged" + }, + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "bc35cc61-5daa-49ad-8783-619d92fcec1d", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-untagged" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..07071dde1ce --- /dev/null +++ b/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ssm_association_query/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ssm_association_tag/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_1.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_1.json new file mode 100644 index 00000000000..d6f28eed1ac --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "Associations": [ + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "b1f5c622-1777-4264-9518-429dbe6fd231", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-tagged" + }, + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "bc35cc61-5daa-49ad-8783-619d92fcec1d", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-untagged" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_2.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_2.json new file mode 100644 index 00000000000..d6f28eed1ac --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListAssociations_2.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "Associations": [ + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "b1f5c622-1777-4264-9518-429dbe6fd231", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-tagged" + }, + { + "Name": "AWS-GatherSoftwareInventory", + "AssociationId": "bc35cc61-5daa-49ad-8783-619d92fcec1d", + "AssociationVersion": "1", + "Targets": [ + { + "Key": "tag:c7n-test", + "Values": [ + "dev" + ] + } + ], + "Overview": { + "Status": "Pending", + "DetailedStatus": "Creating" + }, + "AssociationName": "c7n-test-untagged" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..07071dde1ce --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..2ad1208417a --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_4.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_4.json new file mode 100644 index 00000000000..07071dde1ce --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_4.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_5.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_5.json new file mode 100644 index 00000000000..2ad1208417a --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_5.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_6.json b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_6.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.ListTagsForResource_6.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_association_tag/ssm.RemoveTagsFromResource_1.json b/tests/data/placebo/test_ssm_association_tag/ssm.RemoveTagsFromResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_association_tag/ssm.RemoveTagsFromResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_get_managed_instances/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_get_managed_instances/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_ssm_get_managed_instances/ssm.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_ssm_maintenance_window_query/ssm.DescribeMaintenanceWindows_1.json b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.DescribeMaintenanceWindows_1.json new file mode 100644 index 00000000000..bbada4a6c84 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.DescribeMaintenanceWindows_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "WindowIdentities": [ + { + "WindowId": "mw-09f3ae26ced958b2f", + "Name": "c7n-test-tagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + }, + { + "WindowId": "mw-0f38dc5bab46dfa73", + "Name": "c7n-test-untagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_query/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_1.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_1.json new file mode 100644 index 00000000000..bbada4a6c84 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "WindowIdentities": [ + { + "WindowId": "mw-09f3ae26ced958b2f", + "Name": "c7n-test-tagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + }, + { + "WindowId": "mw-0f38dc5bab46dfa73", + "Name": "c7n-test-untagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_2.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_2.json new file mode 100644 index 00000000000..bbada4a6c84 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.DescribeMaintenanceWindows_2.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "WindowIdentities": [ + { + "WindowId": "mw-09f3ae26ced958b2f", + "Name": "c7n-test-tagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + }, + { + "WindowId": "mw-0f38dc5bab46dfa73", + "Name": "c7n-test-untagged", + "Enabled": true, + "Duration": 2, + "Cutoff": 1, + "Schedule": "cron(0 4 ? * SUN *)", + "NextExecutionTime": "2026-08-23T04:00Z" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_4.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_4.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_4.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_5.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_5.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_5.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_6.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_6.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.ListTagsForResource_6.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.RemoveTagsFromResource_1.json b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.RemoveTagsFromResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_maintenance_window_tag/ssm.RemoveTagsFromResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_1.json b/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_1.json new file mode 100644 index 00000000000..8f5905d78fc --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "Env", + "ResourceId": "i-0585418e43dc3377d", + "ResourceType": "instance", + "Value": "Prod" + }, + { + "Key": "Name", + "ResourceId": "i-0585418e43dc3377d", + "ResourceType": "instance", + "Value": "c7n-test-ec2-ssm" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_2.json b/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_2.json new file mode 100644 index 00000000000..8f5905d78fc --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ec2.DescribeTags_2.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "Env", + "ResourceId": "i-0585418e43dc3377d", + "ResourceType": "instance", + "Value": "Prod" + }, + { + "Key": "Name", + "ResourceId": "i-0585418e43dc3377d", + "ResourceType": "instance", + "Value": "c7n-test-ec2-ssm" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_1.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_1.json new file mode 100644 index 00000000000..71b59e6f779 --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_1.json @@ -0,0 +1,120 @@ +{ + "status_code": 200, + "data": { + "InstanceInformationList": [ + { + "InstanceId": "mi-036e234ffb4be392c", + "PingStatus": "Online", + "LastPingDateTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 20, + "minute": 2, + "second": 9, + "microsecond": 526000 + }, + "AgentVersion": "3.3.4624.0", + "IsLatestVersion": false, + "PlatformType": "Linux", + "PlatformName": "Amazon Linux", + "PlatformVersion": "2023", + "ActivationId": "318b35aa-8092-4687-beb7-182fe8b95c8a", + "IamRole": "SSMServiceRole", + "RegistrationDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 7, + "microsecond": 886000 + }, + "ResourceType": "ManagedInstance", + "IPAddress": "172.31.31.9", + "ComputerName": "ip-172-31-31-9.ec2.internal", + "AssociationStatus": "Success", + "LastAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 51, + "microsecond": 421000 + }, + "LastSuccessfulAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 51, + "microsecond": 421000 + }, + "AssociationOverview": { + "InstanceAssociationStatusAggregatedCount": { + "Success": 1 + } + }, + "SourceId": "mi-036e234ffb4be392c", + "SourceType": "AWS::SSM::ManagedInstance" + }, + { + "InstanceId": "i-0585418e43dc3377d", + "PingStatus": "Online", + "LastPingDateTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 20, + "minute": 3, + "second": 55, + "microsecond": 894000 + }, + "AgentVersion": "3.3.4624.0", + "IsLatestVersion": false, + "PlatformType": "Linux", + "PlatformName": "Amazon Linux", + "PlatformVersion": "2023", + "ResourceType": "EC2Instance", + "IPAddress": "172.31.22.42", + "ComputerName": "ip-172-31-22-42.ec2.internal", + "AssociationStatus": "Success", + "LastAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 39, + "microsecond": 576000 + }, + "LastSuccessfulAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 39, + "microsecond": 576000 + }, + "AssociationOverview": { + "InstanceAssociationStatusAggregatedCount": { + "Success": 1 + } + }, + "SourceId": "i-0585418e43dc3377d", + "SourceType": "AWS::EC2::Instance" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_2.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_2.json new file mode 100644 index 00000000000..71b59e6f779 --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.DescribeInstanceInformation_2.json @@ -0,0 +1,120 @@ +{ + "status_code": 200, + "data": { + "InstanceInformationList": [ + { + "InstanceId": "mi-036e234ffb4be392c", + "PingStatus": "Online", + "LastPingDateTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 20, + "minute": 2, + "second": 9, + "microsecond": 526000 + }, + "AgentVersion": "3.3.4624.0", + "IsLatestVersion": false, + "PlatformType": "Linux", + "PlatformName": "Amazon Linux", + "PlatformVersion": "2023", + "ActivationId": "318b35aa-8092-4687-beb7-182fe8b95c8a", + "IamRole": "SSMServiceRole", + "RegistrationDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 7, + "microsecond": 886000 + }, + "ResourceType": "ManagedInstance", + "IPAddress": "172.31.31.9", + "ComputerName": "ip-172-31-31-9.ec2.internal", + "AssociationStatus": "Success", + "LastAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 51, + "microsecond": 421000 + }, + "LastSuccessfulAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 51, + "microsecond": 421000 + }, + "AssociationOverview": { + "InstanceAssociationStatusAggregatedCount": { + "Success": 1 + } + }, + "SourceId": "mi-036e234ffb4be392c", + "SourceType": "AWS::SSM::ManagedInstance" + }, + { + "InstanceId": "i-0585418e43dc3377d", + "PingStatus": "Online", + "LastPingDateTime": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 20, + "minute": 3, + "second": 55, + "microsecond": 894000 + }, + "AgentVersion": "3.3.4624.0", + "IsLatestVersion": false, + "PlatformType": "Linux", + "PlatformName": "Amazon Linux", + "PlatformVersion": "2023", + "ResourceType": "EC2Instance", + "IPAddress": "172.31.22.42", + "ComputerName": "ip-172-31-22-42.ec2.internal", + "AssociationStatus": "Success", + "LastAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 39, + "microsecond": 576000 + }, + "LastSuccessfulAssociationExecutionDate": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 20, + "hour": 19, + "minute": 58, + "second": 39, + "microsecond": 576000 + }, + "AssociationOverview": { + "InstanceAssociationStatusAggregatedCount": { + "Success": 1 + } + }, + "SourceId": "i-0585418e43dc3377d", + "SourceType": "AWS::EC2::Instance" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_4.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_4.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.ListTagsForResource_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_managed_instance_tag/ssm.RemoveTagsFromResource_1.json b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.RemoveTagsFromResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_managed_instance_tag/ssm.RemoveTagsFromResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_query/ssm.DescribePatchBaselines_1.json b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.DescribePatchBaselines_1.json new file mode 100644 index 00000000000..fa847cd0f39 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.DescribePatchBaselines_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "BaselineIdentities": [ + { + "BaselineId": "pb-07e6af47bc1c9377f", + "BaselineName": "c7n-test-tagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + }, + { + "BaselineId": "pb-0cc2829cd0b558694", + "BaselineName": "c7n-test-untagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_query/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.AddTagsToResource_1.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.AddTagsToResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.AddTagsToResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_1.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_1.json new file mode 100644 index 00000000000..fa847cd0f39 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "BaselineIdentities": [ + { + "BaselineId": "pb-07e6af47bc1c9377f", + "BaselineName": "c7n-test-tagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + }, + { + "BaselineId": "pb-0cc2829cd0b558694", + "BaselineName": "c7n-test-untagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_2.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_2.json new file mode 100644 index 00000000000..fa847cd0f39 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.DescribePatchBaselines_2.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "BaselineIdentities": [ + { + "BaselineId": "pb-07e6af47bc1c9377f", + "BaselineName": "c7n-test-tagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + }, + { + "BaselineId": "pb-0cc2829cd0b558694", + "BaselineName": "c7n-test-untagged", + "OperatingSystem": "WINDOWS", + "DefaultBaseline": false + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_2.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_3.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_3.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_4.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_4.json new file mode 100644 index 00000000000..dfa357546f1 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_4.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Team", + "Value": "Platform" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_5.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_5.json new file mode 100644 index 00000000000..4c807b25d72 --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_5.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "Env", + "Value": "Dev" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_6.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_6.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.ListTagsForResource_6.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.RemoveTagsFromResource_1.json b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.RemoveTagsFromResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_ssm_patch_baseline_tag/ssm.RemoveTagsFromResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ssm_send_command/ec2.DescribeTags_1.json b/tests/data/placebo/test_ssm_send_command/ec2.DescribeTags_1.json new file mode 100644 index 00000000000..c619fee3e45 --- /dev/null +++ b/tests/data/placebo/test_ssm_send_command/ec2.DescribeTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_update_ops_item/ssm.ListTagsForResource_1.json b/tests/data/placebo/test_update_ops_item/ssm.ListTagsForResource_1.json new file mode 100644 index 00000000000..0e8433d32a1 --- /dev/null +++ b/tests/data/placebo/test_update_ops_item/ssm.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} diff --git a/tests/test_ssm.py b/tests/test_ssm.py index bc0304ac2fa..dad09fb17f3 100644 --- a/tests/test_ssm.py +++ b/tests/test_ssm.py @@ -126,6 +126,63 @@ def test_get_resources(self): self.assertEqual(len(resources), 1) self.assertEqual(resources[0]['OpsItemId'], 'oi-5aa4c36439ed') + def test_ops_item_tag(self): + factory = self.replay_flight_data('test_ops_item_tag') + p = self.load_policy({ + 'name': 'ops-item-tag', + 'resource': 'aws.ops-item', + 'filters': [ + {'type': 'value', 'key': 'Title', 'value': 'c7n-test-tag'}, + {'tag:Env': 'absent'}], + 'actions': [ + {'type': 'tag', 'key': 'Env', 'value': 'Dev'}]}, + session_factory=factory, config={'region': 'us-east-1'}) + resources = p.run() + self.assertEqual(len(resources), 1) + client = factory().client('ssm') + tags = client.list_tags_for_resource( + ResourceType='OpsItem', + ResourceId=resources[0]['OpsItemId'])['TagList'] + self.assertEqual(tags, [{'Key': 'Env', 'Value': 'Dev'}]) + + p = self.load_policy({ + 'name': 'ops-item-untag', + 'resource': 'aws.ops-item', + 'filters': [ + {'type': 'value', 'key': 'Title', 'value': 'c7n-test-tag'}, + {'tag:Env': 'present'}], + 'actions': [ + {'type': 'remove-tag', 'tags': ['Env']}]}, + session_factory=factory, config={'region': 'us-east-1'}) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource( + ResourceType='OpsItem', + ResourceId=resources[0]['OpsItemId'])['TagList'] + self.assertEqual(tags, []) + + def test_ops_item_mark_for_op(self): + factory = self.replay_flight_data('test_ops_item_mark_for_op') + p = self.load_policy({ + 'name': 'ops-item-mark', + 'resource': 'aws.ops-item', + 'filters': [ + {'type': 'value', 'key': 'Title', 'value': 'c7n-test-mark'}, + {'tag:custodian_cleanup': 'absent'}], + 'actions': [ + {'type': 'mark-for-op', 'tag': 'custodian_cleanup', + 'op': 'update', 'days': 1}]}, + session_factory=factory, config={'region': 'us-east-1'}) + resources = p.run() + self.assertEqual(len(resources), 1) + client = factory().client('ssm') + tags = client.list_tags_for_resource( + ResourceType='OpsItem', + ResourceId=resources[0]['OpsItemId'])['TagList'] + self.assertEqual(tags[0]['Key'], 'custodian_cleanup') + self.assertTrue(tags[0]['Value'].startswith( + 'Resource does not meet policy: update@')) + class TestSSM(BaseTest): @@ -271,6 +328,244 @@ def test_ssm_get_manager_instances(self): self.assertEqual(len(resources), 1) self.assertEqual(resources[0]["InstanceId"], "mi-1111aa111aa11a111") + def test_ssm_managed_instance_tag_skips_ec2(self): + p = self.load_policy( + { + "name": "ssm-managed-instance-tag", + "resource": "ssm-managed-instance", + "actions": [{"type": "tag", "key": "Env", "value": "Dev"}], + }, + ) + action = p.resource_manager.actions[0] + self.assertEqual( + action.get_taggable( + [{"InstanceId": "mi-1111aa111aa11a111"}, + {"InstanceId": "i-0ba3874e85bb97244"}]), + [{"InstanceId": "mi-1111aa111aa11a111"}]) + source = p.resource_manager.source + self.assertEqual( + source.get_taggable([{"InstanceId": "i-0ba3874e85bb97244"}]), []) + + def test_ssm_managed_instance_tag(self): + session_factory = self.replay_flight_data("test_ssm_managed_instance_tag") + # the account has one hybrid (mi-*) instance without an Env tag + # and one ec2 (i-*) instance carrying Env=Prod in ec2 tags; the + # ec2 instance's tags are visible to the filter, so only the + # hybrid instance matches and gets tagged. + p = self.load_policy( + { + "name": "ssm-managed-instance-tag", + "resource": "ssm-managed-instance", + "filters": [{"tag:Env": "absent"}], + "actions": [{"type": "tag", "key": "Env", "value": "Dev"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["InstanceId"].startswith("mi-")) + client = session_factory().client("ssm") + tags = client.list_tags_for_resource( + ResourceType="ManagedInstance", + ResourceId=resources[0]["InstanceId"])["TagList"] + self.assertEqual(tags, [{"Key": "Env", "Value": "Dev"}]) + + p = self.load_policy( + { + "name": "ssm-managed-instance-untag", + "resource": "ssm-managed-instance", + "filters": [ + {"type": "value", "key": "InstanceId", + "op": "glob", "value": "mi-*"}, + {"tag:Env": "Dev"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource( + ResourceType="ManagedInstance", + ResourceId=resources[0]["InstanceId"])["TagList"] + self.assertEqual(tags, []) + + def test_ssm_patch_baseline_query(self): + session_factory = self.replay_flight_data("test_ssm_patch_baseline_query") + p = self.load_policy( + { + "name": "ssm-patch-baseline-tagged", + "resource": "ssm-patch-baseline", + "filters": [ + {"type": "value", "key": "BaselineName", + "op": "glob", "value": "c7n-test-*"}, + {"tag:Team": "present"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["BaselineName"], "c7n-test-tagged") + self.assertTrue(resources[0]["BaselineId"].startswith("pb-")) + + def test_ssm_patch_baseline_tag(self): + session_factory = self.replay_flight_data("test_ssm_patch_baseline_tag") + p = self.load_policy( + { + "name": "ssm-patch-baseline-tag", + "resource": "ssm-patch-baseline", + "filters": [ + {"type": "value", "key": "BaselineName", + "value": "c7n-test-untagged"}, + {"tag:Env": "absent"}], + "actions": [{"type": "tag", "key": "Env", "value": "Dev"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + client = session_factory().client("ssm") + tags = client.list_tags_for_resource( + ResourceType="PatchBaseline", + ResourceId=resources[0]["BaselineId"])["TagList"] + self.assertEqual(tags, [{"Key": "Env", "Value": "Dev"}]) + + p = self.load_policy( + { + "name": "ssm-patch-baseline-untag", + "resource": "ssm-patch-baseline", + "filters": [ + {"type": "value", "key": "BaselineName", + "value": "c7n-test-untagged"}, + {"tag:Env": "Dev"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource( + ResourceType="PatchBaseline", + ResourceId=resources[0]["BaselineId"])["TagList"] + self.assertEqual(tags, []) + + def test_ssm_maintenance_window_query(self): + session_factory = self.replay_flight_data("test_ssm_maintenance_window_query") + p = self.load_policy( + { + "name": "ssm-maintenance-window-tagged", + "resource": "ssm-maintenance-window", + "filters": [ + {"type": "value", "key": "Name", + "op": "glob", "value": "c7n-test-*"}, + {"tag:Team": "present"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["Name"], "c7n-test-tagged") + self.assertTrue(resources[0]["WindowId"].startswith("mw-")) + + def test_ssm_maintenance_window_tag(self): + session_factory = self.replay_flight_data("test_ssm_maintenance_window_tag") + p = self.load_policy( + { + "name": "ssm-maintenance-window-tag", + "resource": "ssm-maintenance-window", + "filters": [ + {"type": "value", "key": "Name", + "value": "c7n-test-untagged"}, + {"tag:Env": "absent"}], + "actions": [{"type": "tag", "key": "Env", "value": "Dev"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + client = session_factory().client("ssm") + tags = client.list_tags_for_resource( + ResourceType="MaintenanceWindow", + ResourceId=resources[0]["WindowId"])["TagList"] + self.assertEqual(tags, [{"Key": "Env", "Value": "Dev"}]) + + p = self.load_policy( + { + "name": "ssm-maintenance-window-untag", + "resource": "ssm-maintenance-window", + "filters": [ + {"type": "value", "key": "Name", + "value": "c7n-test-untagged"}, + {"tag:Env": "Dev"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource( + ResourceType="MaintenanceWindow", + ResourceId=resources[0]["WindowId"])["TagList"] + self.assertEqual(tags, []) + + def test_ssm_association_query(self): + session_factory = self.replay_flight_data("test_ssm_association_query") + p = self.load_policy( + { + "name": "ssm-association-tagged", + "resource": "ssm-association", + "filters": [ + {"type": "value", "key": "AssociationName", + "op": "glob", "value": "c7n-test-*"}, + {"tag:Team": "present"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["AssociationName"], "c7n-test-tagged") + self.assertTrue(resources[0]["AssociationId"]) + + def test_ssm_association_tag(self): + session_factory = self.replay_flight_data("test_ssm_association_tag") + p = self.load_policy( + { + "name": "ssm-association-tag", + "resource": "ssm-association", + "filters": [ + {"type": "value", "key": "AssociationName", + "value": "c7n-test-untagged"}, + {"tag:Env": "absent"}], + "actions": [{"type": "tag", "key": "Env", "value": "Dev"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + client = session_factory().client("ssm") + tags = client.list_tags_for_resource( + ResourceType="Association", + ResourceId=resources[0]["AssociationId"])["TagList"] + self.assertEqual(tags, [{"Key": "Env", "Value": "Dev"}]) + + p = self.load_policy( + { + "name": "ssm-association-untag", + "resource": "ssm-association", + "filters": [ + {"type": "value", "key": "AssociationName", + "value": "c7n-test-untagged"}, + {"tag:Env": "Dev"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource( + ResourceType="Association", + ResourceId=resources[0]["AssociationId"])["TagList"] + self.assertEqual(tags, []) + def test_get_ssm_documents(self): session_factory = self.replay_flight_data("test_get_ssm_documents") p = self.load_policy( From f2fa1c25768c2a1bb14e0e6487c840d9d1dd3d2f Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:02:31 -0400 Subject: [PATCH 05/65] adding rds-global-cluster resource (#11092) --- c7n/resources/rdscluster.py | 53 ++++++--- c7n/resources/resource_map.py | 1 + .../rds.DescribeGlobalClusters_1.json | 43 +++++++ .../rds.DescribeGlobalClusters_2.json | 48 ++++++++ .../rds.ListTagsForResource_1.json | 12 ++ .../rds.ListTagsForResource_2.json | 7 ++ .../tagging.TagResources_1.json | 7 ++ .../tagging.UntagResources_1.json | 7 ++ .../rds.DescribeGlobalClusters_1.json | 43 +++++++ .../rds.DescribeGlobalClusters_2.json | 48 ++++++++ .../rds.ListTagsForResource_1.json | 12 ++ .../rds.ListTagsForResource_2.json | 7 ++ .../tagging.TagResources_1.json | 7 ++ .../tagging.UntagResources_1.json | 7 ++ .../rds.DescribeGlobalClusters_1.json | 43 +++++++ tests/test_policy.py | 1 - tests/test_rdscluster.py | 105 ++++++++++++++++++ 17 files changed, 436 insertions(+), 15 deletions(-) create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_2.json create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_2.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_rds_global_cluster_tag_filter/rds.DescribeGlobalClusters_1.json diff --git a/c7n/resources/rdscluster.py b/c7n/resources/rdscluster.py index e865d74e9b4..d330f6a697a 100644 --- a/c7n/resources/rdscluster.py +++ b/c7n/resources/rdscluster.py @@ -27,7 +27,16 @@ log = logging.getLogger('custodian.rds-cluster') -class DescribeCluster(DescribeSource): +class TagListDescribeSource(DescribeSource): + """Describe source for rds apis that return tags inline as TagList.""" + + def augment(self, resources): + for r in resources: + r['Tags'] = r.pop('TagList', ()) + return resources + + +class DescribeCluster(TagListDescribeSource): def get_resources(self, ids): resources = chain.from_iterable( @@ -41,11 +50,6 @@ def get_resources(self, ids): ) return list(resources) - def augment(self, resources): - for r in resources: - r['Tags'] = r.pop('TagList', ()) - return resources - class ConfigCluster(ConfigSource): @@ -879,13 +883,6 @@ def process(self, resources, event=None): return results -class DescribeDbShardGroup(DescribeSource): - def augment(self, resources): - for r in resources: - r['Tags'] = r.pop('TagList', ()) - return resources - - @resources.register('rds-db-shard-group') class RDSDbShardGroup(QueryResourceManager): class resource_type(TypeInfo): @@ -899,5 +896,33 @@ class resource_type(TypeInfo): universal_taggable = object() source_mapping = { - 'describe': DescribeDbShardGroup + 'describe': TagListDescribeSource } + + +@resources.register('rds-global-cluster') +class RDSGlobalCluster(QueryResourceManager): + """Resource manager for Aurora global database clusters. + + Global clusters are account level: the same set is returned from + every region and the arn carries no region component. Running a + policy against multiple regions will process each global cluster + once per region, so pin such policies to a single region. + """ + + class resource_type(TypeInfo): + service = 'rds' + arn = 'GlobalClusterArn' + arn_type = 'global-cluster' + arn_separator = ":" + enum_spec = ('describe_global_clusters', 'GlobalClusters', None) + name = id = 'GlobalClusterIdentifier' + config_id = 'GlobalClusterResourceId' + global_resource = True + universal_taggable = object() + cfn_type = config_type = 'AWS::RDS::GlobalCluster' + + source_mapping = { + 'describe': TagListDescribeSource, + 'config': ConfigSource + } diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index 08873841253..4e5b6929041 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -282,6 +282,7 @@ "aws.rds-cluster-param-group": "c7n.resources.rdsparamgroup.RDSClusterParamGroup", "aws.rds-cluster-snapshot": "c7n.resources.rdscluster.RDSClusterSnapshot", "aws.rds-db-shard-group": "c7n.resources.rdscluster.RDSDbShardGroup", + "aws.rds-global-cluster": "c7n.resources.rdscluster.RDSGlobalCluster", "aws.rds-param-group": "c7n.resources.rdsparamgroup.RDSParamGroup", "aws.rds-proxy": "c7n.resources.rds.RDSProxy", "aws.rds-reserved": "c7n.resources.rds.ReservedRDS", diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_1.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_1.json new file mode 100644 index 00000000000..1738974a569 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "GlobalClusters": [ + { + "GlobalClusterIdentifier": "c7n-global-tagged", + "GlobalClusterResourceId": "cluster-d6881b9e440a7828", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-tagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-tagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "Owner", + "Value": "custodian" + } + ] + }, + { + "GlobalClusterIdentifier": "c7n-global-untagged", + "GlobalClusterResourceId": "cluster-a3d672078780a9eb", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-untagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-untagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_2.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_2.json new file mode 100644 index 00000000000..ae1b4c8aa88 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.DescribeGlobalClusters_2.json @@ -0,0 +1,48 @@ +{ + "status_code": 200, + "data": { + "GlobalClusters": [ + { + "GlobalClusterIdentifier": "c7n-global-tagged", + "GlobalClusterResourceId": "cluster-d6881b9e440a7828", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-tagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-tagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "Owner", + "Value": "custodian" + } + ] + }, + { + "GlobalClusterIdentifier": "c7n-global-untagged", + "GlobalClusterResourceId": "cluster-a3d672078780a9eb", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-untagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-untagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "custodian_next", + "Value": "Resource does not meet policy: notify@2026/09/10" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_1.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_1.json new file mode 100644 index 00000000000..2276f6e774b --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "custodian_next", + "Value": "Resource does not meet policy: notify@2026/09/10" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_2.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/rds.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.TagResources_1.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.UntagResources_1.json b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_mark_match_unmark/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_1.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_1.json new file mode 100644 index 00000000000..1738974a569 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "GlobalClusters": [ + { + "GlobalClusterIdentifier": "c7n-global-tagged", + "GlobalClusterResourceId": "cluster-d6881b9e440a7828", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-tagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-tagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "Owner", + "Value": "custodian" + } + ] + }, + { + "GlobalClusterIdentifier": "c7n-global-untagged", + "GlobalClusterResourceId": "cluster-a3d672078780a9eb", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-untagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-untagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_2.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_2.json new file mode 100644 index 00000000000..02779d8dee0 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.DescribeGlobalClusters_2.json @@ -0,0 +1,48 @@ +{ + "status_code": 200, + "data": { + "GlobalClusters": [ + { + "GlobalClusterIdentifier": "c7n-global-tagged", + "GlobalClusterResourceId": "cluster-d6881b9e440a7828", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-tagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-tagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "Owner", + "Value": "custodian" + } + ] + }, + { + "GlobalClusterIdentifier": "c7n-global-untagged", + "GlobalClusterResourceId": "cluster-a3d672078780a9eb", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-untagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-untagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "xyz", + "Value": "hello world" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_1.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_1.json new file mode 100644 index 00000000000..2b7f9bddce0 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "TagList": [ + { + "Key": "xyz", + "Value": "hello world" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_2.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_2.json new file mode 100644 index 00000000000..80b47f8be2f --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/rds.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "TagList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.TagResources_1.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.UntagResources_1.json b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_and_remove/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_rds_global_cluster_tag_filter/rds.DescribeGlobalClusters_1.json b/tests/data/placebo/test_rds_global_cluster_tag_filter/rds.DescribeGlobalClusters_1.json new file mode 100644 index 00000000000..1738974a569 --- /dev/null +++ b/tests/data/placebo/test_rds_global_cluster_tag_filter/rds.DescribeGlobalClusters_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "GlobalClusters": [ + { + "GlobalClusterIdentifier": "c7n-global-tagged", + "GlobalClusterResourceId": "cluster-d6881b9e440a7828", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-tagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-tagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [ + { + "Key": "Owner", + "Value": "custodian" + } + ] + }, + { + "GlobalClusterIdentifier": "c7n-global-untagged", + "GlobalClusterResourceId": "cluster-a3d672078780a9eb", + "GlobalClusterArn": "arn:aws:rds::644160558196:global-cluster:c7n-global-untagged", + "Status": "available", + "Engine": "aurora-postgresql", + "EngineVersion": "17.7", + "EngineLifecycleSupport": "open-source-rds-extended-support", + "StorageEncrypted": false, + "StorageEncryptionType": "sse-rds", + "DeletionProtection": false, + "GlobalClusterMembers": [], + "Endpoint": "c7n-global-untagged.global-ggxgcqjx92ap.global.rds.amazonaws.com", + "TagList": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_policy.py b/tests/test_policy.py index b206b4ed7bb..278baa9b092 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -699,7 +699,6 @@ def test_config_resource_support(self): 'AWS::LookoutMetrics::Alert', 'AWS::MediaPackage::PackagingConfiguration', 'AWS::MediaPackage::PackagingGroup', - 'AWS::RDS::GlobalCluster', 'AWS::RUM::AppMonitor', 'AWS::ResilienceHub::ResiliencyPolicy', 'AWS::RoboMaker::RobotApplicationVersion', diff --git a/tests/test_rdscluster.py b/tests/test_rdscluster.py index 3dbc4fa1593..0a74f25b955 100644 --- a/tests/test_rdscluster.py +++ b/tests/test_rdscluster.py @@ -926,3 +926,108 @@ def test_rds_db_shard_group(self): resources = p.run() self.assertEqual(len(resources), 1) self.assertEqual(resources[0]['DBShardGroupIdentifier'], 'db-shard-1') + + +class RDSGlobalClusterTest(BaseTest): + + def test_rds_global_cluster_tag_filter(self): + session_factory = self.replay_flight_data("test_rds_global_cluster_tag_filter") + p = self.load_policy( + { + "name": "rds-global-cluster-untagged", + "resource": "rds-global-cluster", + "filters": [{"tag:Owner": "absent"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["GlobalClusterIdentifier"], "c7n-global-untagged") + + def test_rds_global_cluster_tag_and_remove(self): + session_factory = self.replay_flight_data("test_rds_global_cluster_tag_and_remove") + client = session_factory().client("rds") + + p = self.load_policy( + { + "name": "rds-global-cluster-tag", + "resource": "rds-global-cluster", + "filters": [{"GlobalClusterIdentifier": "c7n-global-untagged"}], + "actions": [{"type": "tag", "key": "xyz", "value": "hello world"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + arn = resources[0]["GlobalClusterArn"] + tags = client.list_tags_for_resource(ResourceName=arn) + tag_map = {t["Key"]: t["Value"] for t in tags["TagList"]} + self.assertEqual(tag_map.get("xyz"), "hello world") + + p = self.load_policy( + { + "name": "rds-global-cluster-remove-tag", + "resource": "rds-global-cluster", + "filters": [{"tag:xyz": "not-null"}], + "actions": [{"type": "remove-tag", "tags": ["xyz"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + tags = client.list_tags_for_resource(ResourceName=arn) + tag_map = {t["Key"]: t["Value"] for t in tags["TagList"]} + self.assertNotIn("xyz", tag_map) + + def test_rds_global_cluster_mark_match_unmark(self): + session_factory = self.replay_flight_data("test_rds_global_cluster_mark_match_unmark") + client = session_factory().client("rds") + + p = self.load_policy( + { + "name": "rds-global-cluster-mark", + "resource": "rds-global-cluster", + "filters": [{"GlobalClusterIdentifier": "c7n-global-untagged"}], + "actions": [ + { + "type": "mark-for-op", + "tag": "custodian_next", + "days": 1, + "op": "notify", + } + ], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + arn = resources[0]["GlobalClusterArn"] + tags = client.list_tags_for_resource(ResourceName=arn) + tag_map = {t["Key"]: t["Value"] for t in tags["TagList"]} + self.assertIn("custodian_next", tag_map) + + p = self.load_policy( + { + "name": "rds-global-cluster-unmark", + "resource": "rds-global-cluster", + "filters": [ + { + "type": "marked-for-op", + "tag": "custodian_next", + "op": "notify", + "skew": 1, + } + ], + "actions": [{"type": "remove-tag", "tags": ["custodian_next"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + tags = client.list_tags_for_resource(ResourceName=arn) + tag_map = {t["Key"]: t["Value"] for t in tags["TagList"]} + self.assertNotIn("custodian_next", tag_map) From 8e098f40973c790fd59b75015f40489a75a08ec0 Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:58:19 +0530 Subject: [PATCH 06/65] aws - ec2 - ssm-compliance - honor advertised default for states (#10883) The ssm-compliance filter advertises a schema default of ['NON_COMPLIANT'] for the optional 'states' field, but process() read self.data['states'] with a hard subscript. Custodian's Draft7Validator does not inject schema defaults into the policy data, so a policy that omits 'states' passed validation and then raised KeyError: 'states' at runtime. Read the value defensively so the advertised default is applied instead of crashing. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> Co-authored-by: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> --- c7n/resources/ec2.py | 2 +- tests/test_ec2.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py index 9361dad53e8..3fe256fa63d 100644 --- a/c7n/resources/ec2.py +++ b/c7n/resources/ec2.py @@ -1182,7 +1182,7 @@ def process(self, resources, event=None): filters = [ { 'Key': 'Status', - 'Values': self.data['states'], + 'Values': self.data.get('states', ['NON_COMPLIANT']), 'Type': 'EQUAL' }, { diff --git a/tests/test_ec2.py b/tests/test_ec2.py index d1c1562383b..5c7a46ea3d9 100644 --- a/tests/test_ec2.py +++ b/tests/test_ec2.py @@ -499,6 +499,26 @@ def test_ssm_compliance(self): [r['InstanceId'] for r in resources], ['i-0dea82d960d56dc1d', 'i-0ba3874e85bb97244']) + def test_ssm_compliance_states_default(self): + session_factory = self.replay_flight_data('test_ec2_ssm_compliance_filter') + policy = self.load_policy({ + 'name': 'ec2-ssm-compliance', + 'resource': 'aws.ec2', + 'filters': [ + {'type': 'ssm-compliance', + 'compliance_types': [ + 'Association', + 'Patch' + ]}]}, + session_factory=session_factory, + config={'region': 'us-east-2'}) + resources = policy.run() + self.assertEqual(len(resources), 2) + self.assertTrue('c7n:ssm-compliance' in resources[0]) + self.assertEqual( + [r['InstanceId'] for r in resources], + ['i-0dea82d960d56dc1d', 'i-0ba3874e85bb97244']) + def test_ssm_inventory(self): session_factory = self.replay_flight_data("test_ec2_ssm_inventory_filter") p = self.load_policy( From f4d6649534d22862d18e02bd2fff73da509a64ff Mon Sep 17 00:00:00 2001 From: jismail3000 <63059244+jismail3000@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:17:28 -0400 Subject: [PATCH 07/65] wafv2 cloudfront fix (#11127) --- c7n/resources/waf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c7n/resources/waf.py b/c7n/resources/waf.py index 0580a078f08..46b996b7925 100644 --- a/c7n/resources/waf.py +++ b/c7n/resources/waf.py @@ -381,7 +381,7 @@ class WAFV2LoggingFilter(ValueFilter): def process(self, resources, event=None): client = self.manager.get_client() - logging_confs = client.list_logging_configurations(Scope='REGIONAL')[ + logging_confs = client.list_logging_configurations(Scope=self.manager.scope)[ 'LoggingConfigurations' ] resource_map = {r['ARN']: r for r in resources} From 24374976fd9492ec31562a4a9a1bd610071967de Mon Sep 17 00:00:00 2001 From: Rondo <71164236+rondodev@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:17:05 -0400 Subject: [PATCH 08/65] aws - dsql-cluster - register consecutive-aws-backups filter (#11133) --- c7n/resources/dsql.py | 2 ++ ...backup.ListRecoveryPointsByResource_1.json | 28 +++++++++++++++++++ .../dsql.GetCluster_1.json | 28 +++++++++++++++++++ .../dsql.ListClusters_1.json | 12 ++++++++ .../tagging.GetResources_1.json | 18 ++++++++++++ tests/test_dsql.py | 25 +++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/backup.ListRecoveryPointsByResource_1.json create mode 100644 tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.GetCluster_1.json create mode 100644 tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.ListClusters_1.json create mode 100644 tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/tagging.GetResources_1.json diff --git a/c7n/resources/dsql.py b/c7n/resources/dsql.py index 14b58b0d7c9..8e04768bd24 100644 --- a/c7n/resources/dsql.py +++ b/c7n/resources/dsql.py @@ -3,6 +3,7 @@ from botocore.exceptions import ClientError from c7n.actions import BaseAction +from c7n.filters.backup import ConsecutiveAwsBackupsFilter from c7n.filters.iamaccess import CrossAccountAccessFilter from c7n.manager import resources from c7n.query import ( @@ -75,6 +76,7 @@ def process_resource_set(self, client, resources, tag_keys): DsqlCluster.action_registry.register('mark-for-op', TagDelayedAction) +DsqlCluster.filter_registry.register('consecutive-aws-backups', ConsecutiveAwsBackupsFilter) @DsqlCluster.filter_registry.register('cross-account') diff --git a/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/backup.ListRecoveryPointsByResource_1.json b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/backup.ListRecoveryPointsByResource_1.json new file mode 100644 index 00000000000..06c39b0aece --- /dev/null +++ b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/backup.ListRecoveryPointsByResource_1.json @@ -0,0 +1,28 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "RecoveryPoints": [ + { + "RecoveryPointArn": "arn:aws:backup:us-east-1:644160558196:recovery-point:497f416f-4d42-43ae-8ae6-0280ba0d5b6b", + "CreationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 24, + "hour": 9, + "minute": 26, + "second": 1, + "microsecond": 581000 + }, + "Status": "COMPLETED", + "EncryptionKeyArn": "arn:aws:kms:us-east-1:644160558196:key/5d97ffc0-5842-49ce-9daf-6d0a129bf6c8", + "BackupSizeBytes": 0, + "BackupVaultName": "Default", + "IsParent": false, + "ResourceName": "cluster/pnudiyynanlerpax6szgnpv6j4", + "VaultType": "BACKUP_VAULT" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.GetCluster_1.json b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.GetCluster_1.json new file mode 100644 index 00000000000..484683a94ff --- /dev/null +++ b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.GetCluster_1.json @@ -0,0 +1,28 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "identifier": "pnudiyynanlerpax6szgnpv6j4", + "arn": "arn:aws:dsql:us-east-1:644160558196:cluster/pnudiyynanlerpax6szgnpv6j4", + "status": "ACTIVE", + "creationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 24, + "hour": 9, + "minute": 23, + "second": 13, + "microsecond": 133000 + }, + "deletionProtectionEnabled": false, + "tags": { + "Name": "c7n-test" + }, + "encryptionDetails": { + "encryptionType": "AWS_OWNED_KMS_KEY", + "encryptionStatus": "ENABLED" + }, + "endpoint": "pnudiyynanlerpax6szgnpv6j4.dsql.us-east-1.on.aws" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.ListClusters_1.json b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.ListClusters_1.json new file mode 100644 index 00000000000..c64cf978cbb --- /dev/null +++ b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/dsql.ListClusters_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "clusters": [ + { + "identifier": "pnudiyynanlerpax6szgnpv6j4", + "arn": "arn:aws:dsql:us-east-1:644160558196:cluster/pnudiyynanlerpax6szgnpv6j4" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/tagging.GetResources_1.json b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/tagging.GetResources_1.json new file mode 100644 index 00000000000..a3ba85dd476 --- /dev/null +++ b/tests/data/placebo/test_dsql_cluster_consecutive_aws_backups/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:dsql:us-east-1:644160558196:cluster/pnudiyynanlerpax6szgnpv6j4", + "Tags": [ + { + "Key": "Name", + "Value": "c7n-test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_dsql.py b/tests/test_dsql.py index e1e5810702a..9d1b17475be 100644 --- a/tests/test_dsql.py +++ b/tests/test_dsql.py @@ -4,8 +4,11 @@ from unittest import mock from botocore.exceptions import ClientError +from dateutil import parser +import c7n.filters.backup from c7n.resources import dsql +from c7n.testing import mock_datetime_now from .common import BaseTest @@ -75,6 +78,28 @@ def test_delete_force_disables_deletion_protection(self): self.assertEqual(refreshed['deletionProtectionEnabled'], False) self.assertIn(refreshed['status'], ('DELETING', 'DELETED')) + def test_consecutive_aws_backups(self): + factory = self.replay_flight_data("test_dsql_cluster_consecutive_aws_backups") + p = self.load_policy( + { + "name": "dsql-consecutive-aws-backups", + "resource": "aws.dsql-cluster", + "filters": [ + { + "type": "consecutive-aws-backups", + "count": 1, + "period": "days", + "status": "COMPLETED", + } + ], + }, + session_factory=factory, + ) + with mock_datetime_now(parser.parse("2026-09-25T22:00:00+00:00"), c7n.filters.backup): + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["c7n:AwsBackups"][0]["Status"], "COMPLETED") + def test_cross_account_no_policy(self): p = self.load_policy( { From 8a74d4131bfea6a6c29802e41ad4036c17fc1cbe Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Fri, 25 Sep 2026 12:16:41 -0600 Subject: [PATCH 09/65] gcp - add vertex-ai-metadata-store-artifact resource (#11015) --------- Co-authored-by: Claude Opus 5 (1M context) --- tools/c7n_gcp/c7n_gcp/query.py | 18 +- .../c7n_gcp/c7n_gcp/resources/resource_map.py | 2 + tools/c7n_gcp/c7n_gcp/resources/vertexai.py | 95 ++++- ...ores-c7n-store-a-artifacts-artifact_1.json | 23 ++ ...ores-c7n-store-b-artifacts-artifact_1.json | 23 ++ ...etadataStores-c7n-store-a-artifacts_1.json | 34 ++ ...etadataStores-c7n-store-a-artifacts_2.json | 34 ++ ...etadataStores-c7n-store-b-artifacts_1.json | 31 ++ ...etadataStores-c7n-store-b-artifacts_2.json | 31 ++ ...l1-metadataStores-default-artifacts_1.json | 18 + ...l1-metadataStores-default-artifacts_2.json | 18 + ...ocations-us-central1-metadataStores_1.json | 36 ++ ...ocations-us-central1-metadataStores_2.json | 36 ++ ...etadataStores-c7n-store-a-artifacts_1.json | 29 ++ ...etadataStores-c7n-store-b-artifacts_1.json | 26 ++ .../terraform/vertexai_metadata_store/main.tf | 16 + .../vertexai_metadata_store/tf_resources.json | 34 ++ tools/c7n_gcp/tests/test_query.py | 1 + tools/c7n_gcp/tests/test_vertexai.py | 388 ++++++++++++++++++ 19 files changed, 885 insertions(+), 8 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts-artifact_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts-artifact_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json create mode 100644 tools/c7n_gcp/tests/terraform/vertexai_metadata_store/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/vertexai_metadata_store/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/query.py b/tools/c7n_gcp/c7n_gcp/query.py index 25626085284..0162a44ae24 100644 --- a/tools/c7n_gcp/c7n_gcp/query.py +++ b/tools/c7n_gcp/c7n_gcp/query.py @@ -1,6 +1,7 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import contextlib import json import itertools import logging @@ -227,9 +228,11 @@ def check_resource_limit(self, selection_count, population_count): max_resource_limits = MaxResourceLimit(p, selection_count, population_count) return max_resource_limits.check_resource_limits() - def _fetch_resources(self, query): + @contextlib.contextmanager + def _ignore_access_errors(self): + """Log and ignore errors indicating that a resource API is disabled.""" try: - return self.augment(self.source.get_resources(query) or []) + yield except HttpError as e: error_reason, error_code, error_message = extract_errors(e) @@ -237,16 +240,23 @@ def _fetch_resources(self, query): raise if error_code == 403 and 'disabled' in error_message: log.warning(error_message) - return [] + return elif error_reason == 'accessNotConfigured': log.warning( "Resource:%s not available -> Service:%s not enabled on %s", self.type, self.resource_type.service, local_session(self.session_factory).get_default_project()) - return [] + return raise + def _fetch_resources(self, query): + result = [] + with self._ignore_access_errors(): + result = self.augment(self.source.get_resources(query) or []) + + return result + def augment(self, resources): return resources diff --git a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py index 4c8f6335313..37a31589ed5 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py +++ b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py @@ -133,6 +133,8 @@ "c7n_gcp.resources.vertexai.VertexAIHyperparameterTuningJob"), "gcp.vertex-ai-location": "c7n_gcp.resources.vertexai.VertexAILocation", "gcp.vertex-ai-model": "c7n_gcp.resources.vertexai.VertexAIModel", + "gcp.vertex-ai-metadata-store-artifact": ( + "c7n_gcp.resources.vertexai.VertexAIMetadataStoreArtifact"), "gcp.vpc": "c7n_gcp.resources.network.Network", "gcp.zone": "c7n_gcp.resources.compute.Zone", } diff --git a/tools/c7n_gcp/c7n_gcp/resources/vertexai.py b/tools/c7n_gcp/c7n_gcp/resources/vertexai.py index 998b0108ae3..a80936764f5 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/vertexai.py +++ b/tools/c7n_gcp/c7n_gcp/resources/vertexai.py @@ -113,10 +113,11 @@ def _fetch_resources(self, query): # Invoke the client enumeration (Vertex AI API supports pagination) location_resources = [] - for page in client.execute_paged_query(enum_op, params): - page_items = jmespath_search(path, page) - if page_items: - location_resources.extend(page_items) + with self._ignore_access_errors(): + for page in client.execute_paged_query(enum_op, params): + page_items = jmespath_search(path, page) + if page_items: + location_resources.extend(page_items) # Annotate resources with their location for resource in location_resources: @@ -839,6 +840,92 @@ class resource_type(VertexAITypeInfo): urn_component = 'evaluation-run' +@resources.register('vertex-ai-metadata-store-artifact') +class VertexAIMetadataStoreArtifact(VertexAIQueryManager): + """GCP Vertex AI Metadata Store Artifact Resource + + Vertex AI Metadata Store Artifacts represent inputs and outputs (e.g. + datasets, models) tracked for ML lineage, cost attribution, and + governance. + + :example: + + Find artifacts missing an owner label: + + .. code-block:: yaml + + policies: + - name: vertex-ai-artifacts-missing-owner-label + resource: gcp.vertex-ai-metadata-store-artifact + filters: + - type: value + key: labels.owner + value: absent + """ + + class resource_type(VertexAITypeInfo): + component = 'projects.locations.metadataStores.artifacts' + enum_spec = ('list', 'artifacts[]', None) + default_report_fields = ['name', 'displayName', 'createTime', 'updateTime'] + permissions = ( + 'aiplatform.artifacts.list', + 'aiplatform.metadataStores.list', + ) + urn_component = 'metadata-store-artifact' + urn_id_segments = (-3, -1) + + def _fetch_resources(self, query): + """Enumerate artifacts across every location and metadata store. + + Artifacts are children of metadata stores, which are themselves + scoped to a location, so this adds a metadata-store enumeration + step in between the location loop and the artifact listing that + VertexAIQueryManager._fetch_resources otherwise does directly. + """ + session = local_session(self.session_factory) + project = session.get_default_project() + + location_query = self._get_location_query() + location_manager = self.get_resource_manager( + resource_type='vertex-ai-location', + data=({'query': location_query} if location_query else {}) + ) + + all_resources = [] + location_annotation_key = 'c7n:location' + enum_op, path, _ = self.resource_type.enum_spec + + for location_instance in location_manager.resources(): + location = location_instance['name'] + store_client = self.get_location_client( + session, location, 'projects.locations.metadataStores') + artifact_client = None + + parent = f'projects/{project}/locations/{location}' + with self._ignore_access_errors(): + for store_page in store_client.execute_paged_query( + 'list', {'parent': parent}): + stores = jmespath_search('metadataStores[]', store_page) or [] + for store in stores: + if artifact_client is None: + artifact_client = self.get_location_client( + session, location, self.resource_type.component) + artifacts = [] + with self._ignore_access_errors(): + for artifact_page in artifact_client.execute_paged_query( + enum_op, {'parent': store['name']}): + page_items = jmespath_search(path, artifact_page) + if page_items: + artifacts.extend(page_items) + + for artifact in artifacts: + artifact[location_annotation_key] = location_instance + + all_resources.extend(artifacts) + + return all_resources + + @resources.register('vertex-ai-batch-prediction-job') class VertexAIBatchPredictionJob(VertexAIQueryManager): """GCP Vertex AI Batch Prediction Job Resource diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts-artifact_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts-artifact_1.json new file mode 100644 index 00000000000..4d6a72ff2f3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts-artifact_1.json @@ -0,0 +1,23 @@ +{ + "headers": { + "vary": "Origin, X-Origin, Referer", + "content-type": "application/json; charset=UTF-8", + "date": "Wed, 23 Sep 2026 19:43:58 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "404", + "content-length": "129", + "-content-encoding": "gzip" + }, + "body": { + "error": { + "code": 404, + "message": "Requested Metadata Store c7n-store-a not found", + "status": "NOT_FOUND" + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts-artifact_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts-artifact_1.json new file mode 100644 index 00000000000..2c11ec81701 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/delete-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts-artifact_1.json @@ -0,0 +1,23 @@ +{ + "headers": { + "vary": "Origin, X-Origin, Referer", + "content-type": "application/json; charset=UTF-8", + "date": "Wed, 23 Sep 2026 19:43:58 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "404", + "content-length": "129", + "-content-encoding": "gzip" + }, + "body": { + "error": { + "code": 404, + "message": "Requested Metadata Store c7n-store-b not found", + "status": "NOT_FOUND" + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json new file mode 100644 index 00000000000..78a9c42ec28 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json @@ -0,0 +1,34 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "464", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a/artifacts?alt=json" + }, + "body": { + "artifacts": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a/artifacts/artifact", + "displayName": "c7n-test-artifact-labeled", + "etag": "1790192631183", + "labels": { + "owner": "c7n" + }, + "createTime": "2026-09-23T19:43:51.183Z", + "updateTime": "2026-09-23T19:43:51.183Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_2.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_2.json new file mode 100644 index 00000000000..78a9c42ec28 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_2.json @@ -0,0 +1,34 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "464", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a/artifacts?alt=json" + }, + "body": { + "artifacts": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a/artifacts/artifact", + "displayName": "c7n-test-artifact-labeled", + "etag": "1790192631183", + "labels": { + "owner": "c7n" + }, + "createTime": "2026-09-23T19:43:51.183Z", + "updateTime": "2026-09-23T19:43:51.183Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json new file mode 100644 index 00000000000..ba61dcb565a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json @@ -0,0 +1,31 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "416", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b/artifacts?alt=json" + }, + "body": { + "artifacts": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b/artifacts/artifact", + "displayName": "c7n-test-artifact-unlabeled", + "etag": "1790192632057", + "createTime": "2026-09-23T19:43:52.057Z", + "updateTime": "2026-09-23T19:43:52.057Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_2.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_2.json new file mode 100644 index 00000000000..ba61dcb565a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_2.json @@ -0,0 +1,31 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "416", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b/artifacts?alt=json" + }, + "body": { + "artifacts": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b/artifacts/artifact", + "displayName": "c7n-test-artifact-unlabeled", + "etag": "1790192632057", + "createTime": "2026-09-23T19:43:52.057Z", + "updateTime": "2026-09-23T19:43:52.057Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_1.json new file mode 100644 index 00000000000..60f28e23ab6 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_1.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/default/artifacts?alt=json" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_2.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_2.json new file mode 100644 index 00000000000..3ec21b87186 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-default-artifacts_2.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores/default/artifacts?alt=json" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_1.json new file mode 100644 index 00000000000..d3ed7926b5c --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_1.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:52 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "631", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores?alt=json" + }, + "body": { + "metadataStores": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a", + "createTime": "2026-09-23T19:43:36.038232Z", + "updateTime": "2026-09-23T19:43:36.717721Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b", + "createTime": "2026-09-23T19:43:36.016150Z", + "updateTime": "2026-09-23T19:43:36.774454Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/default", + "createTime": "2026-07-18T00:09:48.520332Z", + "updateTime": "2026-07-18T00:09:49.411914Z" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_2.json b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_2.json new file mode 100644 index 00000000000..d40640262d7 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/get-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores_2.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "631", + "-content-encoding": "gzip", + "content-location": "https://us-central1-aiplatform.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/metadataStores?alt=json" + }, + "body": { + "metadataStores": [ + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a", + "createTime": "2026-09-23T19:43:36.038232Z", + "updateTime": "2026-09-23T19:43:36.717721Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b", + "createTime": "2026-09-23T19:43:36.016150Z", + "updateTime": "2026-09-23T19:43:36.774454Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/default", + "createTime": "2026-07-18T00:09:48.520332Z", + "updateTime": "2026-07-18T00:09:49.411914Z" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json new file mode 100644 index 00000000000..b7755ff6dc2 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-a-artifacts_1.json @@ -0,0 +1,29 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:51 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "387", + "-content-encoding": "gzip" + }, + "body": { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-a/artifacts/artifact", + "displayName": "c7n-test-artifact-labeled", + "etag": "1790192631183", + "labels": { + "owner": "c7n" + }, + "createTime": "2026-09-23T19:43:51.183Z", + "updateTime": "2026-09-23T19:43:51.183Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json b/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json new file mode 100644 index 00000000000..5113d202a7e --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/va_artifact/post-us-central1-aiplatform.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-metadataStores-c7n-store-b-artifacts_1.json @@ -0,0 +1,26 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 23 Sep 2026 19:43:52 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "351", + "-content-encoding": "gzip" + }, + "body": { + "name": "projects/cloud-custodian/locations/us-central1/metadataStores/c7n-store-b/artifacts/artifact", + "displayName": "c7n-test-artifact-unlabeled", + "etag": "1790192632057", + "createTime": "2026-09-23T19:43:52.057Z", + "updateTime": "2026-09-23T19:43:52.057Z", + "schemaTitle": "system.Artifact", + "schemaVersion": "0.0.1", + "metadata": {} + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/main.tf b/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/main.tf new file mode 100644 index 00000000000..14052285154 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/main.tf @@ -0,0 +1,16 @@ +provider "google" {} +provider "google-beta" {} + +# Keep the name short: the region and "metadataStores" segments in the API +# path already push recorded flight-data filenames toward Windows' MAX_PATH. +resource "google_vertex_ai_metadata_store" "central_a" { + provider = google-beta + name = "c7n-store-a" + region = "us-central1" +} + +resource "google_vertex_ai_metadata_store" "central_b" { + provider = google-beta + name = "c7n-store-b" + region = "us-central1" +} diff --git a/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/tf_resources.json b/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/tf_resources.json new file mode 100644 index 00000000000..4c88f442e3e --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/vertexai_metadata_store/tf_resources.json @@ -0,0 +1,34 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_vertex_ai_metadata_store": { + "central_a": { + "create_time": "2026-09-23T19:43:36.038232Z", + "deletion_policy": "DELETE", + "description": "", + "encryption_spec": [], + "id": "c7n-store-a", + "name": "c7n-store-a", + "project": "stkl-jim-fulton", + "region": "us-central1", + "state": [], + "timeouts": null, + "update_time": "2026-09-23T19:43:36.717721Z" + }, + "central_b": { + "create_time": "2026-09-23T19:43:36.016150Z", + "deletion_policy": "DELETE", + "description": "", + "encryption_spec": [], + "id": "c7n-store-b", + "name": "c7n-store-b", + "project": "stkl-jim-fulton", + "region": "us-central1", + "state": [], + "timeouts": null, + "update_time": "2026-09-23T19:43:36.774454Z" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_query.py b/tools/c7n_gcp/tests/test_query.py index 5f0271e98eb..5b7bcffa9e1 100644 --- a/tools/c7n_gcp/tests/test_query.py +++ b/tools/c7n_gcp/tests/test_query.py @@ -84,6 +84,7 @@ def test_gcp_resource_metadata_asset_type(): 'vertex-ai-publisher-model', # Catalog resource (no asset inventory type) 'vertex-ai-location', 'vertex-ai-evaluation-run', # No asset inventory type yet + 'vertex-ai-metadata-store-artifact', # Not tracked by asset inventory )) missing = set() for k, v in GoogleCloud.resources.items(): diff --git a/tools/c7n_gcp/tests/test_vertexai.py b/tools/c7n_gcp/tests/test_vertexai.py index 75d62b06685..eb4c60d8283 100644 --- a/tools/c7n_gcp/tests/test_vertexai.py +++ b/tools/c7n_gcp/tests/test_vertexai.py @@ -16,6 +16,20 @@ from c7n_gcp.resources.vertexai import VertexAIEndpoint +def _make_http_error( + code: int, + message: str, + reason: str | None = None, +) -> HttpError: + error = {'code': code, 'message': message} + if reason: + error['errors'] = [{'reason': reason}] + return HttpError( + Mock(status=code, reason=message), + json.dumps({'error': error}).encode(), + ) + + class VertexAIJobs: """Helper for creating/cleaning up ephemeral Vertex AI jobs in tests. @@ -246,6 +260,53 @@ def cleanup(self): print(f'Warning: failed to delete {name} during cleanup: {e}') +class VertexAIMetadataStoreArtifacts: + """Helper for creating/cleaning up ephemeral Metadata Store artifacts. + + The Metadata Store itself is created via Terraform (no Terraform + resource exists for artifacts), so artifacts are created directly + through the API, mirroring VertexAIModels above. + """ + + def __init__(self, test, location='us-central1'): + self.test = test + self.location = location + self.created = [] + + def _client(self): + session = self.test.session_factory() + return session.client( + 'aiplatform', 'v1', 'projects.locations.metadataStores.artifacts', + client_options=ClientOptions( + api_endpoint=f'https://{self.location}-aiplatform.googleapis.com')) + + def create(self, store_name, artifact_id, display_name, labels=None): + """Create an artifact under store_name. + + artifact_id is explicit rather than a service-generated UUID so + recorded flight-data filenames stay well under Windows' MAX_PATH. + """ + client = self._client() + artifact = client.execute_command( + 'create', + {'parent': store_name, + 'artifactId': artifact_id, + 'body': { + 'displayName': display_name, + 'schemaTitle': 'system.Artifact', + 'labels': labels or {}, + }}) + self.created.append((client, artifact['name'])) + return artifact + + def cleanup(self): + for client, name in self.created: + try: + client.execute_command('delete', {'name': name}) + except HttpError: + pass + + def get_test_model_id(project_id, location): """Get full model resource name for testing. @@ -568,6 +629,55 @@ def test_vertexai_endpoint_multi_location(test): assert all('c7n:location' in r for r in resources) +def test_vertexai_query_manager_continues_after_disabled_error(test, caplog): + policy = test.load_policy( + {'name': 'vertexai-endpoint-disabled-location', + 'resource': 'gcp.vertex-ai-endpoint'}) + manager = policy.resource_manager + session = Mock() + session.get_default_project.return_value = 'cloud-custodian' + locations = [ + {'name': 'us-east1'}, + {'name': 'us-central1'}, + ] + location_manager = Mock() + location_manager.resources.return_value = locations + disabled_client = Mock() + disabled_client.execute_paged_query.side_effect = _make_http_error( + 403, + 'Vertex AI API is disabled in us-east1', + 'accessNotConfigured', + ) + endpoint = { + 'name': ('projects/cloud-custodian/locations/us-central1/' + 'endpoints/endpoint-id'), + } + available_client = Mock() + available_client.execute_paged_query.return_value = [ + {'endpoints': [endpoint]}, + ] + + with ( + patch('c7n_gcp.resources.vertexai.local_session', return_value=session), + patch.object( + manager, + 'get_resource_manager', + return_value=location_manager, + ), + patch.object( + manager, + 'get_location_client', + side_effect=[disabled_client, available_client], + ), + caplog.at_level(logging.WARNING), + ): + resources = manager._fetch_resources({}) + + assert resources == [endpoint] + assert endpoint['c7n:location'] == locations[1] + assert 'Vertex AI API is disabled in us-east1' in caplog.text + + def test_vertexai_endpoint_get_urns(test): """Test URN generation for Vertex AI Endpoints. @@ -2160,3 +2270,281 @@ def test_vertexai_hp_tuning_job_field_filters( resources = policy.run() assert len(resources) == 1 assert resources[0]['name'] == job_name + + +def test_vertexai_metadata_store_artifact_resource_registered(test): + """Test that gcp.vertex-ai-metadata-store-artifact resolves as a resource type.""" + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-check', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + assert policy.resource_manager.resource_type.component == ( + 'projects.locations.metadataStores.artifacts') + + +def test_vertexai_metadata_store_artifact_get_urns(test): + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-urns', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + artifacts = [ + {'name': ('projects/cloud-custodian/locations/us-central1/' + 'metadataStores/store-a/artifacts/labeled')}, + {'name': ('projects/cloud-custodian/locations/us-central1/' + 'metadataStores/store-b/artifacts/labeled')}, + ] + + assert policy.resource_manager.resource_type.get_urns( + artifacts, + 'cloud-custodian', + ) == [ + ('gcp:aiplatform:us-central1:cloud-custodian:' + 'metadata-store-artifact/store-a/labeled'), + ('gcp:aiplatform:us-central1:cloud-custodian:' + 'metadata-store-artifact/store-b/labeled'), + ] + + +def test_vertexai_metadata_store_artifact_skips_client_without_stores(test): + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-empty-location', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + manager = policy.resource_manager + session = Mock() + session.get_default_project.return_value = 'cloud-custodian' + location_manager = Mock() + location_manager.resources.return_value = [{'name': 'us-central1'}] + store_client = Mock() + store_client.execute_paged_query.return_value = [{}] + + with ( + patch('c7n_gcp.resources.vertexai.local_session', return_value=session), + patch.object( + manager, + 'get_resource_manager', + return_value=location_manager, + ), + patch.object( + manager, + 'get_location_client', + return_value=store_client, + ) as get_location_client, + ): + resources = manager._fetch_resources({}) + + assert resources == [] + get_location_client.assert_called_once_with( + session, + 'us-central1', + 'projects.locations.metadataStores', + ) + + +def test_vertexai_metadata_store_artifact_continues_after_disabled_store_error( + test, + caplog, +): + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-disabled-location', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + manager = policy.resource_manager + session = Mock() + session.get_default_project.return_value = 'cloud-custodian' + location_manager = Mock() + location_manager.resources.return_value = [ + {'name': 'us-east1'}, + {'name': 'us-central1'}, + ] + disabled_store_client = Mock() + disabled_store_client.execute_paged_query.side_effect = _make_http_error( + 403, + 'Vertex AI API is disabled in us-east1', + 'accessNotConfigured', + ) + store_client = Mock() + store_client.execute_paged_query.return_value = [ + {'metadataStores': [{'name': 'store-name'}]}, + ] + artifact = { + 'name': ('projects/cloud-custodian/locations/us-central1/' + 'metadataStores/store-name/artifacts/artifact-id'), + } + artifact_client = Mock() + artifact_client.execute_paged_query.return_value = [ + {'artifacts': [artifact]}, + ] + + with ( + patch('c7n_gcp.resources.vertexai.local_session', return_value=session), + patch.object( + manager, + 'get_resource_manager', + return_value=location_manager, + ), + patch.object( + manager, + 'get_location_client', + side_effect=[disabled_store_client, store_client, artifact_client], + ), + caplog.at_level(logging.WARNING), + ): + resources = manager._fetch_resources({}) + + assert [resource['name'] for resource in resources] == [artifact['name']] + assert 'Vertex AI API is disabled in us-east1' in caplog.text + + +def test_vertexai_metadata_store_artifact_continues_after_disabled_artifact_error( + test, + caplog, +): + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-disabled-store', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + manager = policy.resource_manager + session = Mock() + session.get_default_project.return_value = 'cloud-custodian' + location_manager = Mock() + location_manager.resources.return_value = [{'name': 'us-central1'}] + store_client = Mock() + store_client.execute_paged_query.return_value = [ + {'metadataStores': [ + {'name': 'disabled-store'}, + {'name': 'available-store'}, + ]}, + ] + artifact = { + 'name': ('projects/cloud-custodian/locations/us-central1/' + 'metadataStores/available-store/artifacts/artifact-id'), + } + artifact_client = Mock() + artifact_client.execute_paged_query.side_effect = [ + _make_http_error( + 403, + 'Vertex AI API is disabled for disabled-store', + 'accessNotConfigured', + ), + [{'artifacts': [artifact]}], + ] + + with ( + patch('c7n_gcp.resources.vertexai.local_session', return_value=session), + patch.object( + manager, + 'get_resource_manager', + return_value=location_manager, + ), + patch.object( + manager, + 'get_location_client', + side_effect=[store_client, artifact_client], + ), + caplog.at_level(logging.WARNING), + ): + resources = manager._fetch_resources({}) + + assert [resource['name'] for resource in resources] == [artifact['name']] + assert 'Vertex AI API is disabled for disabled-store' in caplog.text + + +def test_vertexai_metadata_store_artifact_propagates_other_http_errors(test): + policy = test.load_policy( + {'name': 'vertexai-metadata-store-artifact-api-error', + 'resource': 'gcp.vertex-ai-metadata-store-artifact'}) + manager = policy.resource_manager + session = Mock() + session.get_default_project.return_value = 'cloud-custodian' + location_manager = Mock() + location_manager.resources.return_value = [{'name': 'us-central1'}] + store_client = Mock() + error = _make_http_error(500, 'Vertex AI backend failure') + store_client.execute_paged_query.side_effect = error + + with ( + patch('c7n_gcp.resources.vertexai.local_session', return_value=session), + patch.object( + manager, + 'get_resource_manager', + return_value=location_manager, + ), + patch.object( + manager, + 'get_location_client', + return_value=store_client, + ), + pytest.raises(HttpError) as raised, + ): + manager._fetch_resources({}) + + assert raised.value is error + + +@terraform('vertexai_metadata_store', scope='module') +def test_vertexai_metadata_store_artifact_filtering(test, vertexai_metadata_store): + """Test aggregation and filtering across Metadata Stores. + + Creates one artifact in each Terraform-provisioned store. Both use the + same store-scoped artifact ID; one is labeled and the other is not. + """ + test.session_factory = test.replay_flight_data('va_artifact') + + project = test.session_factory().get_default_project() + store_ids = [ + vertexai_metadata_store[ + 'google_vertex_ai_metadata_store.central_a.name'], + vertexai_metadata_store[ + 'google_vertex_ai_metadata_store.central_b.name'], + ] + store_names = [ + f'projects/{project}/locations/us-central1/metadataStores/{store_id}' + for store_id in store_ids + ] + + artifacts = VertexAIMetadataStoreArtifacts(test) + test.addCleanup(artifacts.cleanup) + if test.recording: + artifacts.create( + store_names[0], + 'artifact', + 'c7n-test-artifact-labeled', + {'owner': 'c7n'}, + ) + artifacts.create( + store_names[1], + 'artifact', + 'c7n-test-artifact-unlabeled', + ) + + policy = test.load_policy( + {'name': 'vertex-ai-artifacts-missing-owner-label', + 'resource': 'gcp.vertex-ai-metadata-store-artifact', + 'query': [{'location': 'us-central1'}], + 'filters': [ + {'type': 'value', 'key': 'displayName', 'op': 'glob', 'value': 'c7n-test-artifact-*'} + ]}, + session_factory=test.session_factory) + + resources = policy.run() + assert len(resources) == 2 + assert { + resource['name'].split('/')[5] + for resource in resources + } == set(store_ids) + assert len(set(policy.resource_manager.get_urns(resources))) == 2 + + unlabeled = [r for r in resources if 'owner' not in r.get('labels', {})] + labeled = [r for r in resources if r.get('labels', {}).get('owner') == 'c7n'] + assert len(labeled) == 1 + assert len(unlabeled) == 1 + + policy = test.load_policy( + {'name': 'vertex-ai-artifacts-missing-owner-label', + 'resource': 'gcp.vertex-ai-metadata-store-artifact', + 'query': [{'location': 'us-central1'}], + 'filters': [ + {'type': 'value', 'key': 'displayName', 'op': 'glob', 'value': 'c7n-test-artifact-*'}, + {'type': 'value', 'key': 'labels.owner', 'value': 'absent'} + ]}, + session_factory=test.session_factory) + + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['displayName'] == 'c7n-test-artifact-unlabeled' From 88594fcfe18ed9043ab264e0f3f97d6b1ed4b262 Mon Sep 17 00:00:00 2001 From: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:33:40 -0400 Subject: [PATCH 10/65] aws - organizations - pull policy types from boto model (#11042) --- c7n/resources/aws.py | 14 +++++++------- c7n/resources/org.py | 44 +++++++++++++++++++++++++++++++++----------- tests/test_org.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/c7n/resources/aws.py b/c7n/resources/aws.py index 7d29350db6b..33284a76e42 100644 --- a/c7n/resources/aws.py +++ b/c7n/resources/aws.py @@ -158,10 +158,13 @@ def _default_bucket_region(options): raise invalid_output +def get_service_shape(service, shape_name): + model = fake_session()._session.get_service_model(service) + return model.shape_for(shape_name) + + def shape_validate(params, shape_name, service): - session = fake_session()._session - model = session.get_service_model(service) - shape = model.shape_for(shape_name) + shape = get_service_shape(service, shape_name) validator = ParamValidator() report = validator.validate(params, shape) if report.has_errors(): @@ -953,8 +956,5 @@ def _expand_shape_schema(shape): schema[member] = member_schema return schema - session = fake_session()._session - model = session.get_service_model(service) - shape = model.shape_for(shape_name) - + shape = get_service_shape(service, shape_name) return _expand_shape_schema(shape) diff --git a/c7n/resources/org.py b/c7n/resources/org.py index 295a12b36b6..6bfa89869cc 100644 --- a/c7n/resources/org.py +++ b/c7n/resources/org.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from concurrent.futures import as_completed +from functools import lru_cache import json import logging import os @@ -14,7 +15,7 @@ from c7n.exceptions import PolicyValidationError from c7n.filters import Filter, ValueFilter, ListItemFilter from c7n.query import QueryResourceManager, TypeInfo, DescribeSource -from c7n.resources.aws import AWS +from c7n.resources.aws import AWS, get_service_shape from c7n.tags import universal_augment from c7n.utils import local_session, type_schema @@ -24,6 +25,23 @@ ORG_ACCOUNT_SESSION_NAME = "CustodianOrgAccount" +@lru_cache(maxsize=None) +def get_policy_types(): + return tuple(get_service_shape("organizations", "PolicyType").enum) + + +class PolicyTypeValidator: + def validate(self): + ptype = self.data["policy-type"] + valid = get_policy_types() + if ptype not in valid: + raise PolicyValidationError( + "%s invalid policy-type: %s, valid types: %s" + % (self.type, ptype, ", ".join(valid)) + ) + return self + + class OrgAccess: org_session = None @@ -59,12 +77,6 @@ def get_org_session(self): @AWS.resources.register("org-policy") class OrgPolicy(QueryResourceManager, OrgAccess): - policy_types = ( - "SERVICE_CONTROL_POLICY", - "TAG_POLICY", - "BACKUP_POLICY", - "AISERVICES_OPT_OUT_POLICY", - ) class resource_type(TypeInfo): service = "organizations" @@ -193,12 +205,17 @@ def parse_query(self): @OrgUnit.filter_registry.register("policy") @OrgAccount.filter_registry.register("policy") -class PolicyFilter(ListItemFilter): +class PolicyFilter(PolicyTypeValidator, ListItemFilter): schema = type_schema( "policy", required=["policy-type"], **{ - "policy-type": {"enum": OrgPolicy.policy_types}, + "policy-type": { + "type": "string", + "description": ( + "Organizations policy type, validated against the botocore PolicyType enum" + ), + }, "inherited": {"type": "boolean"}, "attrs": {"$ref": "#/definitions/filters_common/list_item_attrs"}, "count": {"type": "number"}, @@ -267,7 +284,7 @@ def get_item_values(self, resource): @OrgAccount.action_registry.register("set-policy") @OrgUnit.action_registry.register("set-policy") -class SetPolicy(Action): +class SetPolicy(PolicyTypeValidator, Action): """Set a policy on an org unit or account .. code-block:: yaml @@ -322,7 +339,12 @@ class SetPolicy(Action): **{ "name": {"type": "string"}, "description": {"type": "string"}, - "policy-type": {"enum": OrgPolicy.policy_types}, + "policy-type": { + "type": "string", + "description": ( + "Organizations policy type, validated against the botocore PolicyType enum" + ), + }, "contents": {"type": "object"}, "tags": {"$ref": "#/definitions/string_dict"}, }, diff --git a/tests/test_org.py b/tests/test_org.py index 2b3d9b02b35..419afe87c3c 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -10,6 +10,7 @@ from botocore.exceptions import ClientError +from c7n.exceptions import PolicyValidationError from c7n.executor import MainThreadExecutor from c7n.resources import org as org_module @@ -534,3 +535,31 @@ def test_policy_query(policy_org, test): assert p.resource_manager.parse_query() == {"Filter": "SERVICE_CONTROL_POLICY"} resources = p.run() assert {r["Name"] for r in resources} == {"FullAWSAccess", "ec2-diet"} + + +def test_policy_filter_invalid_type(test): + with pytest.raises(PolicyValidationError) as ecm: + test.load_policy( + { + "name": "policy-enforce", + "resource": "aws.org-unit", + "filters": [{"type": "policy", "policy-type": "BOGUS_POLICY"}], + }, + validate=False, + ) + assert "BOGUS_POLICY" in str(ecm.value) + + +def test_set_policy_invalid_type(test): + with pytest.raises(PolicyValidationError) as ecm: + test.load_policy( + { + "name": "policy-enforce", + "resource": "aws.org-unit", + "actions": [ + {"type": "set-policy", "name": "gopher", "policy-type": "BOGUS_POLICY"} + ], + }, + validate=False, + ) + assert "BOGUS_POLICY" in str(ecm.value) From be9e869d1b5b9b9e0819f59dea9fbfc693b365c7 Mon Sep 17 00:00:00 2001 From: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:36:05 -0400 Subject: [PATCH 11/65] aws - add aiops investigation groups resource type (#10899) --- c7n/resources/aiops.py | 130 ++++++++++++++++++ c7n/resources/resource_map.py | 1 + .../aiops.DeleteInvestigationGroup_1.json | 6 + .../aiops.GetInvestigationGroup_1.json | 18 +++ .../aiops.ListInvestigationGroups_1.json | 12 ++ .../aiops.ListInvestigationGroups_2.json | 7 + .../tagging.GetResources_1.json | 8 ++ .../aiops.GetInvestigationGroup_1.json | 18 +++ .../aiops.ListInvestigationGroups_1.json | 12 ++ .../tagging.GetResources_1.json | 18 +++ .../aiops.GetInvestigationGroup_1.json | 18 +++ .../aiops.ListInvestigationGroups_1.json | 12 ++ .../tagging.GetResources_1.json | 18 +++ .../aiops.GetInvestigationGroup_1.json | 18 +++ .../aiops.GetInvestigationGroup_2.json | 18 +++ .../aiops.ListInvestigationGroups_1.json | 12 ++ .../aiops.ListInvestigationGroups_2.json | 12 ++ .../aiops.ListTagsForResource_1.json | 10 ++ .../aiops.ListTagsForResource_2.json | 9 ++ .../aiops.TagResource_1.json | 6 + .../aiops.UntagResource_1.json | 6 + .../tagging.GetResources_1.json | 18 +++ .../tagging.GetResources_2.json | 22 +++ tests/test_aiops.py | 111 +++++++++++++++ 24 files changed, 520 insertions(+) create mode 100644 c7n/resources/aiops.py create mode 100644 tests/data/placebo/test_aiops_investigation_group_delete/aiops.DeleteInvestigationGroup_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_delete/aiops.GetInvestigationGroup_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_2.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_delete/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.GetInvestigationGroup_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.ListInvestigationGroups_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_encryption_filter/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_query/aiops.GetInvestigationGroup_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_query/aiops.ListInvestigationGroups_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_query/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_2.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_2.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.TagResource_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.UntagResource_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_2.json create mode 100644 tests/test_aiops.py diff --git a/c7n/resources/aiops.py b/c7n/resources/aiops.py new file mode 100644 index 00000000000..eb044dac369 --- /dev/null +++ b/c7n/resources/aiops.py @@ -0,0 +1,130 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from c7n.manager import resources +from c7n.query import QueryResourceManager, TypeInfo, DescribeWithResourceTags +from c7n.tags import Tag, RemoveTag, TagActionFilter, TagDelayedAction +from c7n.utils import local_session, type_schema, get_retry +from c7n.actions import BaseAction + + +@resources.register('aiops-investigation-group') +class AIOpsInvestigationGroup(QueryResourceManager): + """AWS AIOps Investigation Group + + :example: + + Find investigation groups not using a customer managed KMS key: + + .. code-block:: yaml + + policies: + - name: aiops-investigation-group-no-cmk + resource: aws.aiops-investigation-group + filters: + - type: value + key: encryptionConfiguration.type + value: AWS_OWNED_KEY + """ + + class resource_type(TypeInfo): + service = 'aiops' + enum_spec = ('list_investigation_groups', 'investigationGroups', None) + detail_spec = ('get_investigation_group', 'identifier', 'arn', None) + id = arn = 'arn' + name = 'name' + date = 'lastModifiedAt' + permissions_augment = ('aiops:ListTagsForResource',) + + retry = staticmethod(get_retry(( + 'ThrottlingException', + 'ConflictException', + 'InternalServerException', + ))) + + source_mapping = {'describe': DescribeWithResourceTags} + + +@AIOpsInvestigationGroup.action_registry.register('tag') +class TagInvestigationGroup(Tag): + """Add tags to an investigation group. + + :example: + + .. code-block:: yaml + + policies: + - name: aiops-investigation-group-tag + resource: aws.aiops-investigation-group + actions: + - type: tag + key: Env + value: production + """ + + permissions = ('aiops:TagResource',) + + def process_resource_set(self, client, resource_set, tags): + new_tags = {t['Key']: t['Value'] for t in tags} + for r in resource_set: + self.manager.retry( + client.tag_resource, resourceArn=r['arn'], tags=new_tags) + + +@AIOpsInvestigationGroup.action_registry.register('remove-tag') +class RemoveTagInvestigationGroup(RemoveTag): + """Remove tags from an investigation group. + + :example: + + .. code-block:: yaml + + policies: + - name: aiops-investigation-group-remove-tag + resource: aws.aiops-investigation-group + actions: + - type: remove-tag + tags: + - Env + """ + + permissions = ('aiops:UntagResource',) + + def process_resource_set(self, client, resource_set, tag_keys): + for r in resource_set: + self.manager.retry( + client.untag_resource, resourceArn=r['arn'], tagKeys=tag_keys) + + +AIOpsInvestigationGroup.filter_registry.register('marked-for-op', TagActionFilter) +AIOpsInvestigationGroup.action_registry.register('mark-for-op', TagDelayedAction) + + +@AIOpsInvestigationGroup.action_registry.register('delete') +class DeleteInvestigationGroup(BaseAction): + """Delete an investigation group. + + :example: + + .. code-block:: yaml + + policies: + - name: aiops-delete-untagged-investigation-groups + resource: aws.aiops-investigation-group + filters: + - 'tag:Owner': absent + actions: + - type: delete + """ + + schema = type_schema('delete') + permissions = ('aiops:DeleteInvestigationGroup',) + + def process(self, resources): + client = local_session(self.manager.session_factory).client('aiops') + for r in resources: + self.manager.retry( + client.delete_investigation_group, + identifier=r['arn'], + ignore_err_codes=('ResourceNotFoundException',), + ) diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index 4e5b6929041..a93c52530c2 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -6,6 +6,7 @@ "aws.account": "c7n.resources.account.Account", "aws.acm-certificate": "c7n.resources.acm.Certificate", "aws.advisor-check": "c7n.resources.support.AdvisorCheck", + "aws.aiops-investigation-group": "c7n.resources.aiops.AIOpsInvestigationGroup", "aws.airflow": "c7n.resources.airflow.ApacheAirflow", "aws.alarm": "c7n.resources.cw.Alarm", "aws.ami": "c7n.resources.ami.AMI", diff --git a/tests/data/placebo/test_aiops_investigation_group_delete/aiops.DeleteInvestigationGroup_1.json b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.DeleteInvestigationGroup_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.DeleteInvestigationGroup_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_delete/aiops.GetInvestigationGroup_1.json b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.GetInvestigationGroup_1.json new file mode 100644 index 00000000000..587e7c4c056 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.GetInvestigationGroup_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "createdBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "createdAt": 1784661319165, + "lastModifiedBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "lastModifiedAt": 1784661319165, + "name": "DefaultInvestigationGroup", + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/T1ewGc2UhVD00Sjc", + "roleArn": "arn:aws:iam::644160558196:role/AIOpsRole-DefaultInvestigationGroup-bgqsjg", + "encryptionConfiguration": { + "type": "AWS_OWNED_KEY" + }, + "retentionInDays": 90, + "isCloudTrailEventHistoryEnabled": true + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_1.json b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_1.json new file mode 100644 index 00000000000..bd0c91b267a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [ + { + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/T1ewGc2UhVD00Sjc", + "name": "DefaultInvestigationGroup" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_2.json b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_2.json new file mode 100644 index 00000000000..6a2d23df6ac --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_delete/aiops.ListInvestigationGroups_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_delete/tagging.GetResources_1.json b/tests/data/placebo/test_aiops_investigation_group_delete/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_delete/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.GetInvestigationGroup_1.json b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.GetInvestigationGroup_1.json new file mode 100644 index 00000000000..e252b23bc5a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.GetInvestigationGroup_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "createdBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "createdAt": 1784660047573, + "lastModifiedBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "lastModifiedAt": 1784660047573, + "name": "DefaultInvestigationGroup", + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "roleArn": "arn:aws:iam::644160558196:role/AIOpsRole-DefaultInvestigationGroup-d07fdn", + "encryptionConfiguration": { + "type": "AWS_OWNED_KEY" + }, + "retentionInDays": 90, + "isCloudTrailEventHistoryEnabled": true + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.ListInvestigationGroups_1.json b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.ListInvestigationGroups_1.json new file mode 100644 index 00000000000..f7504bf7a87 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/aiops.ListInvestigationGroups_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [ + { + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "name": "DefaultInvestigationGroup" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_encryption_filter/tagging.GetResources_1.json b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/tagging.GetResources_1.json new file mode 100644 index 00000000000..4c680168bd6 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_encryption_filter/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "Tags": [ + { + "Key": "test", + "Value": "true" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_query/aiops.GetInvestigationGroup_1.json b/tests/data/placebo/test_aiops_investigation_group_query/aiops.GetInvestigationGroup_1.json new file mode 100644 index 00000000000..e252b23bc5a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_query/aiops.GetInvestigationGroup_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "createdBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "createdAt": 1784660047573, + "lastModifiedBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "lastModifiedAt": 1784660047573, + "name": "DefaultInvestigationGroup", + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "roleArn": "arn:aws:iam::644160558196:role/AIOpsRole-DefaultInvestigationGroup-d07fdn", + "encryptionConfiguration": { + "type": "AWS_OWNED_KEY" + }, + "retentionInDays": 90, + "isCloudTrailEventHistoryEnabled": true + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_query/aiops.ListInvestigationGroups_1.json b/tests/data/placebo/test_aiops_investigation_group_query/aiops.ListInvestigationGroups_1.json new file mode 100644 index 00000000000..f7504bf7a87 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_query/aiops.ListInvestigationGroups_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [ + { + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "name": "DefaultInvestigationGroup" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_query/tagging.GetResources_1.json b/tests/data/placebo/test_aiops_investigation_group_query/tagging.GetResources_1.json new file mode 100644 index 00000000000..4c680168bd6 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_query/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "Tags": [ + { + "Key": "test", + "Value": "true" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_1.json new file mode 100644 index 00000000000..e252b23bc5a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "createdBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "createdAt": 1784660047573, + "lastModifiedBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "lastModifiedAt": 1784660047573, + "name": "DefaultInvestigationGroup", + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "roleArn": "arn:aws:iam::644160558196:role/AIOpsRole-DefaultInvestigationGroup-d07fdn", + "encryptionConfiguration": { + "type": "AWS_OWNED_KEY" + }, + "retentionInDays": 90, + "isCloudTrailEventHistoryEnabled": true + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_2.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_2.json new file mode 100644 index 00000000000..e252b23bc5a --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.GetInvestigationGroup_2.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "createdBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "createdAt": 1784660047573, + "lastModifiedBy": "arn:aws:sts::644160558196:assumed-role/AWSReservedSSO_Org003-Developer_e3d7100119dd6aee/eah523", + "lastModifiedAt": 1784660047573, + "name": "DefaultInvestigationGroup", + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "roleArn": "arn:aws:iam::644160558196:role/AIOpsRole-DefaultInvestigationGroup-d07fdn", + "encryptionConfiguration": { + "type": "AWS_OWNED_KEY" + }, + "retentionInDays": 90, + "isCloudTrailEventHistoryEnabled": true + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_1.json new file mode 100644 index 00000000000..f7504bf7a87 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [ + { + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "name": "DefaultInvestigationGroup" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_2.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_2.json new file mode 100644 index 00000000000..f7504bf7a87 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListInvestigationGroups_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "investigationGroups": [ + { + "arn": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "name": "DefaultInvestigationGroup" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_1.json new file mode 100644 index 00000000000..0848e233f24 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": { + "test": "true", + "Env": "production" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_2.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_2.json new file mode 100644 index 00000000000..73892542602 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.ListTagsForResource_2.json @@ -0,0 +1,9 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": { + "test": "true" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.TagResource_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.TagResource_1.json new file mode 100644 index 00000000000..4636bf11195 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.TagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 204, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.UntagResource_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.UntagResource_1.json new file mode 100644 index 00000000000..4636bf11195 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/aiops.UntagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 204, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_1.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_1.json new file mode 100644 index 00000000000..4c680168bd6 --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "Tags": [ + { + "Key": "test", + "Value": "true" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_2.json b/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_2.json new file mode 100644 index 00000000000..52d816e733b --- /dev/null +++ b/tests/data/placebo/test_aiops_investigation_group_tag_untag/tagging.GetResources_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:aiops:us-east-1:644160558196:investigation-group/zKIq4Uc78GVq3PFg", + "Tags": [ + { + "Key": "test", + "Value": "true" + }, + { + "Key": "Env", + "Value": "production" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_aiops.py b/tests/test_aiops.py new file mode 100644 index 00000000000..374964a33df --- /dev/null +++ b/tests/test_aiops.py @@ -0,0 +1,111 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from .common import BaseTest + + +class AIOpsInvestigationGroupTest(BaseTest): + + def test_aiops_investigation_group_query(self): + session_factory = self.replay_flight_data("test_aiops_investigation_group_query") + p = self.load_policy( + { + "name": "aiops-investigation-group-query", + "resource": "aws.aiops-investigation-group", + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + r = resources[0] + # detail_spec enrichment from get_investigation_group + self.assertIn("arn", r) + self.assertIn("name", r) + self.assertIn("retentionInDays", r) + self.assertIn("encryptionConfiguration", r) + self.assertIsInstance(r["Tags"], list) + + def test_aiops_investigation_group_encryption_filter(self): + session_factory = self.replay_flight_data( + "test_aiops_investigation_group_encryption_filter" + ) + p = self.load_policy( + { + "name": "aiops-investigation-group-no-cmk", + "resource": "aws.aiops-investigation-group", + "filters": [ + { + "type": "value", + "key": "encryptionConfiguration.type", + "value": "AWS_OWNED_KEY", + } + ], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual( + resources[0]["encryptionConfiguration"]["type"], "AWS_OWNED_KEY" + ) + + def test_aiops_investigation_group_tag_untag(self): + session_factory = self.replay_flight_data( + "test_aiops_investigation_group_tag_untag" + ) + p = self.load_policy( + { + "name": "aiops-investigation-group-tag", + "resource": "aws.aiops-investigation-group", + "filters": [{"tag:Env": "absent"}], + "actions": [{"type": "tag", "key": "Env", "value": "production"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["arn"] + + client = session_factory().client("aiops") + tags = client.list_tags_for_resource(resourceArn=arn)["tags"] + self.assertEqual(tags.get("Env"), "production") + + p = self.load_policy( + { + "name": "aiops-investigation-group-remove-tag", + "resource": "aws.aiops-investigation-group", + "filters": [{"tag:Env": "production"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + tags = client.list_tags_for_resource(resourceArn=arn)["tags"] + self.assertNotIn("Env", tags) + + def test_aiops_investigation_group_delete(self): + session_factory = self.replay_flight_data( + "test_aiops_investigation_group_delete" + ) + p = self.load_policy( + { + "name": "aiops-investigation-group-delete", + "resource": "aws.aiops-investigation-group", + "filters": [{"tag:Owner": "absent"}], + "actions": [{"type": "delete"}], + }, + session_factory=session_factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + deleted_arn = resources[0]["arn"] + + client = session_factory().client("aiops") + remaining = [ + g["arn"] + for g in client.list_investigation_groups().get( + "investigationGroups", [] + ) + ] + self.assertNotIn(deleted_arn, remaining) From 5217be4f33bcf29cdd8ebf2871107ede28eb5f64 Mon Sep 17 00:00:00 2001 From: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:38:11 -0400 Subject: [PATCH 12/65] aws - add qbusiness resource types (#10990) Resources * qbusiness-application (top-level) * qbusiness-index, qbusiness-plugin, qbusiness-data-accessor (children of application) * qbusiness-data-source (enumerated across each application/index pair) Filters / Actions * cross-account filter on qbusiness-application * Universal tagging on all resources --- c7n/resources/qbusiness.py | 204 +++++++++++++ c7n/resources/resource_map.py | 5 + .../qbusiness.GetApplication_1.json | 37 +++ .../qbusiness.ListApplications_1.json | 34 +++ .../tagging.GetResources_1.json | 13 + .../qbusiness.GetApplication_1.json | 33 +++ .../qbusiness.GetPolicy_1.json | 7 + .../qbusiness.ListApplications_1.json | 34 +++ .../tagging.GetResources_1.json | 13 + .../qbusiness.GetApplication_1.json | 37 +++ .../qbusiness.GetApplication_2.json | 37 +++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListApplications_2.json | 34 +++ .../qbusiness.ListTagsForResource_1.json | 12 + .../qbusiness.ListTagsForResource_2.json | 7 + .../tagging.GetResources_1.json | 13 + .../tagging.GetResources_2.json | 18 ++ .../tagging.TagResources_1.json | 7 + .../tagging.UntagResources_1.json | 7 + .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListDataAccessors_1.json | 35 +++ .../tagging.GetResources_1.json | 18 ++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListApplications_2.json | 34 +++ .../qbusiness.ListDataAccessors_1.json | 35 +++ .../qbusiness.ListDataAccessors_2.json | 35 +++ .../qbusiness.ListTagsForResource_1.json | 16 + .../qbusiness.ListTagsForResource_2.json | 12 + .../tagging.GetResources_1.json | 18 ++ .../tagging.GetResources_2.json | 22 ++ .../tagging.TagResources_1.json | 7 + .../tagging.UntagResources_1.json | 7 + .../qbusiness.GetDataSource_1.json | 71 +++++ .../qbusiness.GetIndex_1.json | 104 +++++++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListDataSources_1.json | 34 +++ .../qbusiness.ListIndices_1.json | 33 +++ .../tagging.GetResources_1.json | 13 + .../tagging.GetResources_2.json | 8 + .../qbusiness.GetDataSource_1.json | 71 +++++ .../qbusiness.GetIndex_1.json | 104 +++++++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListDataSources_1.json | 34 +++ .../qbusiness.ListIndices_1.json | 33 +++ .../tagging.GetResources_1.json | 13 + .../tagging.GetResources_2.json | 8 + .../qbusiness.GetDataSource_1.json | 71 +++++ .../qbusiness.GetDataSource_2.json | 71 +++++ .../qbusiness.GetIndex_1.json | 104 +++++++ .../qbusiness.GetIndex_2.json | 104 +++++++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListApplications_2.json | 34 +++ .../qbusiness.ListDataSources_1.json | 34 +++ .../qbusiness.ListDataSources_2.json | 34 +++ .../qbusiness.ListIndices_1.json | 33 +++ .../qbusiness.ListIndices_2.json | 33 +++ .../qbusiness.ListTagsForResource_1.json | 12 + .../qbusiness.ListTagsForResource_2.json | 7 + .../tagging.GetResources_1.json | 13 + .../tagging.GetResources_2.json | 8 + .../tagging.GetResources_3.json | 13 + .../tagging.GetResources_4.json | 18 ++ .../tagging.TagResources_1.json | 7 + .../tagging.UntagResources_1.json | 7 + .../qbusiness.GetIndex_1.json | 104 +++++++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListIndices_1.json | 33 +++ .../tagging.GetResources_1.json | 8 + .../qbusiness.GetIndex_1.json | 104 +++++++ .../qbusiness.GetIndex_2.json | 104 +++++++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListApplications_2.json | 34 +++ .../qbusiness.ListIndices_1.json | 33 +++ .../qbusiness.ListIndices_2.json | 33 +++ .../qbusiness.ListTagsForResource_1.json | 12 + .../qbusiness.ListTagsForResource_2.json | 7 + .../tagging.GetResources_1.json | 8 + .../tagging.GetResources_2.json | 18 ++ .../tagging.TagResources_1.json | 7 + .../tagging.UntagResources_1.json | 7 + .../qbusiness.GetPlugin_1.json | 44 +++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListPlugins_1.json | 36 +++ .../tagging.GetResources_1.json | 18 ++ .../qbusiness.GetPlugin_1.json | 44 +++ .../qbusiness.GetPlugin_2.json | 44 +++ .../qbusiness.ListApplications_1.json | 34 +++ .../qbusiness.ListApplications_2.json | 34 +++ .../qbusiness.ListPlugins_1.json | 36 +++ .../qbusiness.ListPlugins_2.json | 36 +++ .../qbusiness.ListTagsForResource_1.json | 16 + .../qbusiness.ListTagsForResource_2.json | 12 + .../tagging.GetResources_1.json | 18 ++ .../tagging.GetResources_2.json | 22 ++ .../tagging.TagResources_1.json | 7 + .../tagging.UntagResources_1.json | 7 + tests/test_qbusiness.py | 274 ++++++++++++++++++ 97 files changed, 3420 insertions(+) create mode 100644 c7n/resources/qbusiness.py create mode 100644 tests/data/placebo/test_qbusiness_application/qbusiness.GetApplication_1.json create mode 100644 tests/data/placebo/test_qbusiness_application/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_application/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetApplication_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetPolicy_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_cross_account/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_2.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_2.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_application_tag/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListDataAccessors_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/qbusiness.GetDataSource_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/qbusiness.GetIndex_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/qbusiness.ListDataSources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/qbusiness.ListIndices_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetDataSource_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetIndex_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListDataSources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListIndices_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_3.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_4.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_data_source_tag/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_index/qbusiness.GetIndex_1.json create mode 100644 tests/data/placebo/test_qbusiness_index/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_index/qbusiness.ListIndices_1.json create mode 100644 tests/data/placebo/test_qbusiness_index/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_2.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_2.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_2.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_index_tag/tagging.UntagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin/qbusiness.GetPlugin_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin/qbusiness.ListPlugins_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_2.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_2.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_2.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/tagging.TagResources_1.json create mode 100644 tests/data/placebo/test_qbusiness_plugin_tag/tagging.UntagResources_1.json create mode 100644 tests/test_qbusiness.py diff --git a/c7n/resources/qbusiness.py b/c7n/resources/qbusiness.py new file mode 100644 index 00000000000..b40d7cac579 --- /dev/null +++ b/c7n/resources/qbusiness.py @@ -0,0 +1,204 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from c7n.filters import CrossAccountAccessFilter +from c7n.manager import resources +from c7n.query import ( + ChildDescribeSource, + ChildResourceManager, + DescribeWithResourceTags, + QueryResourceManager, + RetryPageIterator, + TypeInfo, +) +from c7n.tags import universal_augment +from c7n.utils import local_session + + +@resources.register('qbusiness-application') +class QBusinessApplication(QueryResourceManager): + """Amazon Q Business Application""" + + class resource_type(TypeInfo): + service = 'qbusiness' + enum_spec = ('list_applications', 'applications', None) + detail_spec = ('get_application', 'applicationId', 'applicationId', None) + id = 'applicationId' + arn = 'applicationArn' + name = 'displayName' + date = 'updatedAt' + cfn_type = 'AWS::QBusiness::Application' + universal_taggable = object() + + source_mapping = {'describe': DescribeWithResourceTags} + + +@QBusinessApplication.filter_registry.register('cross-account') +class QBusinessApplicationCrossAccount(CrossAccountAccessFilter): + """Filter Q Business applications whose resource-based permission policy + grants cross-account access. + + :example: + + .. code-block:: yaml + + policies: + - name: qbusiness-app-cross-account + resource: aws.qbusiness-application + filters: + - type: cross-account + """ + + policy_attribute = 'c7n:Policy' + permissions = ('qbusiness:GetPolicy',) + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('qbusiness') + for r in resources: + if self.policy_attribute in r: + continue + try: + r[self.policy_attribute] = self.manager.retry( + client.get_policy, applicationId=r['applicationId']).get('policy') + except client.exceptions.ResourceNotFoundException: + r[self.policy_attribute] = None + return super().process(resources, event) + + +class QBusinessChildDescribe(ChildDescribeSource): + + def get_query(self): + return super().get_query(capture_parent_id=True) + + def augment(self, resources): + client = local_session(self.manager.session_factory).client('qbusiness') + rtype = self.manager.resource_type + op = getattr(client, rtype.detail_op) + results = [] + for app_id, r in resources: + detail = self.manager.retry( + op, applicationId=app_id, **{rtype.id: r[rtype.id]}) + detail.pop('ResponseMetadata', None) + results.append(detail) + return universal_augment(self.manager, results) + + +@resources.register('qbusiness-index') +class QBusinessIndex(ChildResourceManager): + """Amazon Q Business Index""" + + class resource_type(TypeInfo): + service = 'qbusiness' + enum_spec = ('list_indices', 'indices', None) + detail_op = 'get_index' + parent_spec = ('qbusiness-application', 'applicationId', None) + id = 'indexId' + arn = 'indexArn' + name = 'displayName' + date = 'updatedAt' + cfn_type = 'AWS::QBusiness::Index' + permissions_augment = ('qbusiness:GetIndex',) + supports_trailevents = True + universal_taggable = object() + + source_mapping = {'describe-child': QBusinessChildDescribe} + + +@resources.register('qbusiness-plugin') +class QBusinessPlugin(ChildResourceManager): + """Amazon Q Business Plugin""" + + class resource_type(TypeInfo): + service = 'qbusiness' + enum_spec = ('list_plugins', 'plugins', None) + detail_op = 'get_plugin' + parent_spec = ('qbusiness-application', 'applicationId', None) + id = 'pluginId' + arn = 'pluginArn' + name = 'displayName' + date = 'updatedAt' + cfn_type = 'AWS::QBusiness::Plugin' + permissions_augment = ('qbusiness:GetPlugin',) + supports_trailevents = True + universal_taggable = object() + + source_mapping = {'describe-child': QBusinessChildDescribe} + + +class DescribeQDataAccessor(ChildDescribeSource): + + def augment(self, resources): + return universal_augment(self.manager, resources) + + +@resources.register('qbusiness-data-accessor') +class QBusinessDataAccessor(ChildResourceManager): + """Amazon Q Business Data Accessor""" + + class resource_type(TypeInfo): + service = 'qbusiness' + enum_spec = ('list_data_accessors', 'dataAccessors', None) + parent_spec = ('qbusiness-application', 'applicationId', None) + id = 'dataAccessorId' + arn = 'dataAccessorArn' + name = 'displayName' + date = 'updatedAt' + supports_trailevents = True + universal_taggable = object() + + source_mapping = {'describe-child': DescribeQDataAccessor} + + +class DescribeQDataSource(ChildDescribeSource): + + def resources(self, query): + client = local_session(self.manager.session_factory).client('qbusiness') + paginator = client.get_paginator('list_data_sources') + paginator.PAGE_ITERATOR_CLS = RetryPageIterator + indices = self.manager.get_resource_manager('qbusiness-index').resources() + results = [] + for idx in indices: + app_id = idx['applicationId'] + index_id = idx['indexId'] + for page in paginator.paginate(applicationId=app_id, indexId=index_id): + for ds in page.get('dataSources', []): + ds['applicationId'] = app_id + ds['indexId'] = index_id + results.append(ds) + return results + + def get_resources(self, ids): + # event mode: ids may be raw dataSourceId values or full arns + wanted = {i.rsplit('/', 1)[-1] for i in ids} + return [r for r in self.resources({}) if r['dataSourceId'] in wanted] + + def augment(self, resources): + client = local_session(self.manager.session_factory).client('qbusiness') + for r in resources: + detail = self.manager.retry( + client.get_data_source, + applicationId=r['applicationId'], + indexId=r['indexId'], + dataSourceId=r['dataSourceId']) + detail.pop('ResponseMetadata', None) + r.update(detail) + return universal_augment(self.manager, resources) + + +@resources.register('qbusiness-data-source') +class QBusinessDataSource(ChildResourceManager): + """Amazon Q Business Data Source""" + + class resource_type(TypeInfo): + service = 'qbusiness' + enum_spec = ('list_data_sources', 'dataSources', None) + parent_spec = ('qbusiness-index', 'indexId', None) + id = 'dataSourceId' + arn = 'dataSourceArn' + name = 'displayName' + date = 'updatedAt' + cfn_type = 'AWS::QBusiness::DataSource' + permissions_augment = ('qbusiness:GetDataSource',) + supports_trailevents = True + universal_taggable = object() + + source_mapping = {'describe-child': DescribeQDataSource} diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index a93c52530c2..f36dc1eb4d0 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -268,6 +268,11 @@ "aws.peering-connection": "c7n.resources.vpc.PeeringConnection", "aws.pinpoint-app": "c7n.resources.pinpoint.PinpointApp", "aws.prefix-list": "c7n.resources.vpc.PrefixList", + "aws.qbusiness-application": "c7n.resources.qbusiness.QBusinessApplication", + "aws.qbusiness-data-accessor": "c7n.resources.qbusiness.QBusinessDataAccessor", + "aws.qbusiness-data-source": "c7n.resources.qbusiness.QBusinessDataSource", + "aws.qbusiness-index": "c7n.resources.qbusiness.QBusinessIndex", + "aws.qbusiness-plugin": "c7n.resources.qbusiness.QBusinessPlugin", "aws.qldb": "c7n.resources.qldb.QLDB", "aws.quicksight-account": "c7n.resources.quicksight.QuicksightAccount", "aws.quicksight-dashboard": "c7n.resources.quicksight.QuicksightDashboard", diff --git a/tests/data/placebo/test_qbusiness_application/qbusiness.GetApplication_1.json b/tests/data/placebo/test_qbusiness_application/qbusiness.GetApplication_1.json new file mode 100644 index 00000000000..b112dab9c47 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application/qbusiness.GetApplication_1.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "applicationArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "identityType": "ANONYMOUS", + "roleArn": "arn:aws:iam::644160558196:role/aws-service-role/qbusiness.amazonaws.com/AWSServiceRoleForQBusiness", + "status": "ACTIVE", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "error": {}, + "attachmentsConfiguration": { + "attachmentsControlMode": "DISABLED" + }, + "autoSubscriptionConfiguration": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_application/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_application/tagging.GetResources_1.json new file mode 100644 index 00000000000..db0dc068cb1 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetApplication_1.json b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetApplication_1.json new file mode 100644 index 00000000000..dceb87d7d22 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetApplication_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "applicationArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "identityType": "AWS_IAM_IDC", + "roleArn": "arn:aws:iam::644160558196:role/service-role/QBusiness-c7n-test-app", + "status": "ACTIVE", + "description": "application used to exercise the cross-account filter", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } +} diff --git a/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetPolicy_1.json b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetPolicy_1.json new file mode 100644 index 00000000000..8d7db9cfaa4 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.GetPolicy_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"c7ntestexternalaccount\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123456789012:role/q-business-cross-account-test\"},\"Action\":\"qbusiness:SearchRelevantContent\",\"Resource\":\"arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f\"}]}" + } +} diff --git a/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..e2cbf5bb4b9 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_cross_account/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_application_cross_account/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_application_cross_account/tagging.GetResources_1.json new file mode 100644 index 00000000000..cd34c7a980f --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_cross_account/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_1.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_1.json new file mode 100644 index 00000000000..b112dab9c47 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_1.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "applicationArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "identityType": "ANONYMOUS", + "roleArn": "arn:aws:iam::644160558196:role/aws-service-role/qbusiness.amazonaws.com/AWSServiceRoleForQBusiness", + "status": "ACTIVE", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "error": {}, + "attachmentsConfiguration": { + "attachmentsControlMode": "DISABLED" + }, + "autoSubscriptionConfiguration": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_2.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_2.json new file mode 100644 index 00000000000..b112dab9c47 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.GetApplication_2.json @@ -0,0 +1,37 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "applicationArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "identityType": "ANONYMOUS", + "roleArn": "arn:aws:iam::644160558196:role/aws-service-role/qbusiness.amazonaws.com/AWSServiceRoleForQBusiness", + "status": "ACTIVE", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "error": {}, + "attachmentsConfiguration": { + "attachmentsControlMode": "DISABLED" + }, + "autoSubscriptionConfiguration": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_2.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_2.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListApplications_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_1.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_1.json new file mode 100644 index 00000000000..4794bc76524 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "Env", + "value": "Test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_2.json b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_2.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/qbusiness.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..db0dc068cb1 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_2.json new file mode 100644 index 00000000000..3fcd7616b0a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/tagging.GetResources_2.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f", + "Tags": [ + { + "Key": "Env", + "Value": "Test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/tagging.TagResources_1.json b/tests/data/placebo/test_qbusiness_application_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_application_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_qbusiness_application_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_application_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListDataAccessors_1.json b/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListDataAccessors_1.json new file mode 100644 index 00000000000..9fdd79b683f --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor/qbusiness.ListDataAccessors_1.json @@ -0,0 +1,35 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataAccessors": [ + { + "displayName": "c7n-test-accessor", + "dataAccessorId": "b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "dataAccessorArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "idcApplicationArn": "arn:aws:sso::644160558196:application/ssoins-7223d3f1a9e04b6c/apl-a1b2c3d4e5f60718", + "principal": "arn:aws:iam::123456789012:role/c7n-test-accessor-principal", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_data_accessor/tagging.GetResources_1.json new file mode 100644 index 00000000000..d5b0753aba6 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_2.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_2.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListApplications_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_1.json new file mode 100644 index 00000000000..9fdd79b683f --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_1.json @@ -0,0 +1,35 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataAccessors": [ + { + "displayName": "c7n-test-accessor", + "dataAccessorId": "b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "dataAccessorArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "idcApplicationArn": "arn:aws:sso::644160558196:application/ssoins-7223d3f1a9e04b6c/apl-a1b2c3d4e5f60718", + "principal": "arn:aws:iam::123456789012:role/c7n-test-accessor-principal", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_2.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_2.json new file mode 100644 index 00000000000..9fdd79b683f --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListDataAccessors_2.json @@ -0,0 +1,35 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataAccessors": [ + { + "displayName": "c7n-test-accessor", + "dataAccessorId": "b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "dataAccessorArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "idcApplicationArn": "arn:aws:sso::644160558196:application/ssoins-7223d3f1a9e04b6c/apl-a1b2c3d4e5f60718", + "principal": "arn:aws:iam::123456789012:role/c7n-test-accessor-principal", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_1.json new file mode 100644 index 00000000000..3cf4782be12 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "team", + "value": "knowledge-platform" + }, + { + "key": "Env", + "value": "Test" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_2.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_2.json new file mode 100644 index 00000000000..061bd79b961 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/qbusiness.ListTagsForResource_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "team", + "value": "knowledge-platform" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..d5b0753aba6 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_2.json new file mode 100644 index 00000000000..bdf4c943d1d --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.GetResources_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/data-accessor/b52c1e88-6a4d-4f39-8c07-1e9a3d5f7b20", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + }, + { + "Key": "Env", + "Value": "Test" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.TagResources_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..41aaa7a1749 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "FailedResourcesMap": {} + } +} diff --git a/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..41aaa7a1749 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_accessor_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "FailedResourcesMap": {} + } +} diff --git a/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetDataSource_1.json b/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetDataSource_1.json new file mode 100644 index 00000000000..903fe3e5311 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetDataSource_1.json @@ -0,0 +1,71 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "dataSourceArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4/data-source/c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "displayName": "c7n-test-source", + "type": "S3V2", + "configuration": { + "metadataFilesPrefix": null, + "deletionProtectionConfiguration": { + "enableDeletionProtection": false, + "deletionProtectionThreshold": "1" + }, + "accessControlConfiguration": { + "aclConfigurationFilePath": null + }, + "connectionConfiguration": { + "bucketName": "aws-cloudtrail-logs-644160558196-786c5163", + "bucketOwnerAccountId": "644160558196" + }, + "filterConfiguration": { + "inclusionPatterns": [], + "maxFileSizeInMegaBytes": "50", + "inclusionPrefixes": [], + "exclusionPatterns": [], + "exclusionPrefixes": [] + }, + "type": "S3V2", + "version": "1.0.0" + }, + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "description": "", + "status": "ACTIVE", + "syncSchedule": "cron(13 20 1 * ? *)", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-q-business-role", + "error": {}, + "mediaExtractionConfiguration": { + "imageExtractionConfiguration": { + "imageExtractionStatus": "DISABLED" + }, + "audioExtractionConfiguration": { + "audioExtractionStatus": "DISABLED" + }, + "videoExtractionConfiguration": { + "videoExtractionStatus": "DISABLED" + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetIndex_1.json b/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetIndex_1.json new file mode 100644 index 00000000000..63806922c79 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/qbusiness.GetIndex_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 20859271, + "indexedTextDocumentCount": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListDataSources_1.json b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListDataSources_1.json new file mode 100644 index 00000000000..9bb8dc28acb --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListDataSources_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataSources": [ + { + "displayName": "c7n-test-source", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "type": "S3V2", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListIndices_1.json b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListIndices_1.json new file mode 100644 index 00000000000..c407113f4d5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/qbusiness.ListIndices_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_1.json new file mode 100644 index 00000000000..3a6a9dde001 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetDataSource_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetDataSource_1.json new file mode 100644 index 00000000000..903fe3e5311 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetDataSource_1.json @@ -0,0 +1,71 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "dataSourceArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4/data-source/c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "displayName": "c7n-test-source", + "type": "S3V2", + "configuration": { + "metadataFilesPrefix": null, + "deletionProtectionConfiguration": { + "enableDeletionProtection": false, + "deletionProtectionThreshold": "1" + }, + "accessControlConfiguration": { + "aclConfigurationFilePath": null + }, + "connectionConfiguration": { + "bucketName": "aws-cloudtrail-logs-644160558196-786c5163", + "bucketOwnerAccountId": "644160558196" + }, + "filterConfiguration": { + "inclusionPatterns": [], + "maxFileSizeInMegaBytes": "50", + "inclusionPrefixes": [], + "exclusionPatterns": [], + "exclusionPrefixes": [] + }, + "type": "S3V2", + "version": "1.0.0" + }, + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "description": "", + "status": "ACTIVE", + "syncSchedule": "cron(13 20 1 * ? *)", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-q-business-role", + "error": {}, + "mediaExtractionConfiguration": { + "imageExtractionConfiguration": { + "imageExtractionStatus": "DISABLED" + }, + "audioExtractionConfiguration": { + "audioExtractionStatus": "DISABLED" + }, + "videoExtractionConfiguration": { + "videoExtractionStatus": "DISABLED" + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetIndex_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetIndex_1.json new file mode 100644 index 00000000000..63806922c79 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.GetIndex_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 20859271, + "indexedTextDocumentCount": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListDataSources_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListDataSources_1.json new file mode 100644 index 00000000000..9bb8dc28acb --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListDataSources_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataSources": [ + { + "displayName": "c7n-test-source", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "type": "S3V2", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListIndices_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListIndices_1.json new file mode 100644 index 00000000000..c407113f4d5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/qbusiness.ListIndices_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_1.json new file mode 100644 index 00000000000..3a6a9dde001 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_no_vpc/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_1.json new file mode 100644 index 00000000000..903fe3e5311 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_1.json @@ -0,0 +1,71 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "dataSourceArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4/data-source/c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "displayName": "c7n-test-source", + "type": "S3V2", + "configuration": { + "metadataFilesPrefix": null, + "deletionProtectionConfiguration": { + "enableDeletionProtection": false, + "deletionProtectionThreshold": "1" + }, + "accessControlConfiguration": { + "aclConfigurationFilePath": null + }, + "connectionConfiguration": { + "bucketName": "aws-cloudtrail-logs-644160558196-786c5163", + "bucketOwnerAccountId": "644160558196" + }, + "filterConfiguration": { + "inclusionPatterns": [], + "maxFileSizeInMegaBytes": "50", + "inclusionPrefixes": [], + "exclusionPatterns": [], + "exclusionPrefixes": [] + }, + "type": "S3V2", + "version": "1.0.0" + }, + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "description": "", + "status": "ACTIVE", + "syncSchedule": "cron(13 20 1 * ? *)", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-q-business-role", + "error": {}, + "mediaExtractionConfiguration": { + "imageExtractionConfiguration": { + "imageExtractionStatus": "DISABLED" + }, + "audioExtractionConfiguration": { + "audioExtractionStatus": "DISABLED" + }, + "videoExtractionConfiguration": { + "videoExtractionStatus": "DISABLED" + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_2.json new file mode 100644 index 00000000000..903fe3e5311 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetDataSource_2.json @@ -0,0 +1,71 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "dataSourceArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4/data-source/c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "displayName": "c7n-test-source", + "type": "S3V2", + "configuration": { + "metadataFilesPrefix": null, + "deletionProtectionConfiguration": { + "enableDeletionProtection": false, + "deletionProtectionThreshold": "1" + }, + "accessControlConfiguration": { + "aclConfigurationFilePath": null + }, + "connectionConfiguration": { + "bucketName": "aws-cloudtrail-logs-644160558196-786c5163", + "bucketOwnerAccountId": "644160558196" + }, + "filterConfiguration": { + "inclusionPatterns": [], + "maxFileSizeInMegaBytes": "50", + "inclusionPrefixes": [], + "exclusionPatterns": [], + "exclusionPrefixes": [] + }, + "type": "S3V2", + "version": "1.0.0" + }, + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "description": "", + "status": "ACTIVE", + "syncSchedule": "cron(13 20 1 * ? *)", + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-q-business-role", + "error": {}, + "mediaExtractionConfiguration": { + "imageExtractionConfiguration": { + "imageExtractionStatus": "DISABLED" + }, + "audioExtractionConfiguration": { + "audioExtractionStatus": "DISABLED" + }, + "videoExtractionConfiguration": { + "videoExtractionStatus": "DISABLED" + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_1.json new file mode 100644 index 00000000000..63806922c79 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 20859271, + "indexedTextDocumentCount": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_2.json new file mode 100644 index 00000000000..63806922c79 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.GetIndex_2.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 20859271, + "indexedTextDocumentCount": 1 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_2.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListApplications_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_1.json new file mode 100644 index 00000000000..9bb8dc28acb --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataSources": [ + { + "displayName": "c7n-test-source", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "type": "S3V2", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_2.json new file mode 100644 index 00000000000..9bb8dc28acb --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListDataSources_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "dataSources": [ + { + "displayName": "c7n-test-source", + "dataSourceId": "c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "type": "S3V2", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 41, + "microsecond": 422000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_1.json new file mode 100644 index 00000000000..c407113f4d5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_2.json new file mode 100644 index 00000000000..c407113f4d5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListIndices_2.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 16, + "minute": 13, + "second": 44, + "microsecond": 42000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_1.json new file mode 100644 index 00000000000..4794bc76524 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "Env", + "value": "Test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_2.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/qbusiness.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..3a6a9dde001 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_3.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_3.json new file mode 100644 index 00000000000..3a6a9dde001 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_3.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "Tags": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_4.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_4.json new file mode 100644 index 00000000000..14e186df415 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.GetResources_4.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4/data-source/c9fb5b7a-fdaa-4565-ad89-8062951f7038", + "Tags": [ + { + "Key": "Env", + "Value": "Test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.TagResources_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_data_source_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_data_source_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index/qbusiness.GetIndex_1.json b/tests/data/placebo/test_qbusiness_index/qbusiness.GetIndex_1.json new file mode 100644 index 00000000000..636ffe5e448 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index/qbusiness.GetIndex_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 0, + "indexedTextDocumentCount": 0 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_index/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index/qbusiness.ListIndices_1.json b/tests/data/placebo/test_qbusiness_index/qbusiness.ListIndices_1.json new file mode 100644 index 00000000000..35a75b6a9e4 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index/qbusiness.ListIndices_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_index/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_1.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_1.json new file mode 100644 index 00000000000..636ffe5e448 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 0, + "indexedTextDocumentCount": 0 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_2.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_2.json new file mode 100644 index 00000000000..636ffe5e448 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.GetIndex_2.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "displayName": "c7n-test", + "indexArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "status": "ACTIVE", + "type": "STARTER", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "capacityConfiguration": { + "units": 1 + }, + "documentAttributeConfigurations": [ + { + "name": "_authors", + "type": "STRING_LIST", + "search": "DISABLED" + }, + { + "name": "_category", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_created_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_data_source_id", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_data_source_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_document_title", + "type": "STRING", + "search": "ENABLED" + }, + { + "name": "_file_type", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_language_code", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_last_updated_at", + "type": "DATE", + "search": "DISABLED" + }, + { + "name": "_source_uri", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_version", + "type": "STRING", + "search": "DISABLED" + }, + { + "name": "_view_count", + "type": "NUMBER", + "search": "DISABLED" + } + ], + "error": {}, + "indexStatistics": { + "textDocumentStatistics": { + "indexedTextBytes": 0, + "indexedTextDocumentCount": 0 + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_2.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_2.json new file mode 100644 index 00000000000..ad679564a50 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListApplications_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "QBusiness-application-j2r1l", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 2, + "second": 32, + "microsecond": 747000 + }, + "status": "ACTIVE", + "identityType": "ANONYMOUS" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_1.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_1.json new file mode 100644 index 00000000000..35a75b6a9e4 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_2.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_2.json new file mode 100644 index 00000000000..35a75b6a9e4 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListIndices_2.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "indices": [ + { + "displayName": "c7n-test", + "indexId": "33c51a98-a798-4424-bf2a-ecd12c2796a4", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 8, + "day": 10, + "hour": 15, + "minute": 51, + "second": 55, + "microsecond": 314000 + }, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_1.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_1.json new file mode 100644 index 00000000000..4794bc76524 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "Env", + "value": "Test" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_2.json b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_2.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/qbusiness.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_2.json new file mode 100644 index 00000000000..3b45a3d3b15 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/tagging.GetResources_2.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/index/33c51a98-a798-4424-bf2a-ecd12c2796a4", + "Tags": [ + { + "Key": "Env", + "Value": "Test" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/tagging.TagResources_1.json b/tests/data/placebo/test_qbusiness_index_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_index_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_qbusiness_index_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..bd2dead8598 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_index_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "FailedResourcesMap": {}, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_qbusiness_plugin/qbusiness.GetPlugin_1.json b/tests/data/placebo/test_qbusiness_plugin/qbusiness.GetPlugin_1.json new file mode 100644 index 00000000000..a0fa5abc06b --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin/qbusiness.GetPlugin_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "authConfiguration": { + "noAuthConfiguration": {} + }, + "customPluginConfiguration": { + "description": "c7n test custom plugin", + "apiSchemaType": "OPEN_API_V3", + "apiSchema": { + "payload": "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"c7n-test-plugin\",\"version\":\"1.0.0\"},\"servers\":[{\"url\":\"https://c7n-test-plugin.example.com\"}],\"paths\":{\"/search\":{\"get\":{\"operationId\":\"search\",\"description\":\"search\",\"responses\":{\"200\":{\"description\":\"ok\"}}}}}}" + } + }, + "buildStatus": "READY", + "pluginArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "state": "ENABLED", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListPlugins_1.json b/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListPlugins_1.json new file mode 100644 index 00000000000..fd93697c8b0 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin/qbusiness.ListPlugins_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "plugins": [ + { + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "state": "ENABLED", + "buildStatus": "READY", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_plugin/tagging.GetResources_1.json new file mode 100644 index 00000000000..6e0c3851211 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_1.json new file mode 100644 index 00000000000..a0fa5abc06b --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "authConfiguration": { + "noAuthConfiguration": {} + }, + "customPluginConfiguration": { + "description": "c7n test custom plugin", + "apiSchemaType": "OPEN_API_V3", + "apiSchema": { + "payload": "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"c7n-test-plugin\",\"version\":\"1.0.0\"},\"servers\":[{\"url\":\"https://c7n-test-plugin.example.com\"}],\"paths\":{\"/search\":{\"get\":{\"operationId\":\"search\",\"description\":\"search\",\"responses\":{\"200\":{\"description\":\"ok\"}}}}}}" + } + }, + "buildStatus": "READY", + "pluginArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "state": "ENABLED", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_2.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_2.json new file mode 100644 index 00000000000..a0fa5abc06b --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.GetPlugin_2.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "authConfiguration": { + "noAuthConfiguration": {} + }, + "customPluginConfiguration": { + "description": "c7n test custom plugin", + "apiSchemaType": "OPEN_API_V3", + "apiSchema": { + "payload": "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"c7n-test-plugin\",\"version\":\"1.0.0\"},\"servers\":[{\"url\":\"https://c7n-test-plugin.example.com\"}],\"paths\":{\"/search\":{\"get\":{\"operationId\":\"search\",\"description\":\"search\",\"responses\":{\"200\":{\"description\":\"ok\"}}}}}}" + } + }, + "buildStatus": "READY", + "pluginArn": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "state": "ENABLED", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_1.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_1.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_2.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_2.json new file mode 100644 index 00000000000..7c6a573cb96 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListApplications_2.json @@ -0,0 +1,34 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "applications": [ + { + "displayName": "c7n-test-app", + "applicationId": "ed27546d-6f28-4480-820e-6592404f720f", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + }, + "status": "ACTIVE", + "identityType": "AWS_IAM_IDC" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_1.json new file mode 100644 index 00000000000..fd93697c8b0 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_1.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "plugins": [ + { + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "state": "ENABLED", + "buildStatus": "READY", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_2.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_2.json new file mode 100644 index 00000000000..fd93697c8b0 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListPlugins_2.json @@ -0,0 +1,36 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "plugins": [ + { + "pluginId": "7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "displayName": "c7n-test-plugin", + "type": "CUSTOM", + "serverUrl": "https://c7n-test-plugin.example.com", + "state": "ENABLED", + "buildStatus": "READY", + "createdAt": { + "__class__": "datetime", + "year": 2025, + "month": 11, + "day": 14, + "hour": 9, + "minute": 23, + "second": 41, + "microsecond": 512000 + }, + "updatedAt": { + "__class__": "datetime", + "year": 2026, + "month": 5, + "day": 2, + "hour": 16, + "minute": 7, + "second": 18, + "microsecond": 884000 + } + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_1.json new file mode 100644 index 00000000000..3cf4782be12 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "team", + "value": "knowledge-platform" + }, + { + "key": "Env", + "value": "Test" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_2.json b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_2.json new file mode 100644 index 00000000000..061bd79b961 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/qbusiness.ListTagsForResource_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "key": "team", + "value": "knowledge-platform" + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_1.json new file mode 100644 index 00000000000..6e0c3851211 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_1.json @@ -0,0 +1,18 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_2.json b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_2.json new file mode 100644 index 00000000000..eef5c60896b --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.GetResources_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "PaginationToken": "", + "ResourceTagMappingList": [ + { + "ResourceARN": "arn:aws:qbusiness:us-east-1:644160558196:application/ed27546d-6f28-4480-820e-6592404f720f/plugin/7d4e9a02-3f61-4c8b-9e15-6a2f0c1b8d34", + "Tags": [ + { + "Key": "team", + "Value": "knowledge-platform" + }, + { + "Key": "Env", + "Value": "Test" + } + ] + } + ] + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/tagging.TagResources_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.TagResources_1.json new file mode 100644 index 00000000000..41aaa7a1749 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.TagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "FailedResourcesMap": {} + } +} diff --git a/tests/data/placebo/test_qbusiness_plugin_tag/tagging.UntagResources_1.json b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.UntagResources_1.json new file mode 100644 index 00000000000..41aaa7a1749 --- /dev/null +++ b/tests/data/placebo/test_qbusiness_plugin_tag/tagging.UntagResources_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "FailedResourcesMap": {} + } +} diff --git a/tests/test_qbusiness.py b/tests/test_qbusiness.py new file mode 100644 index 00000000000..5409654e5b9 --- /dev/null +++ b/tests/test_qbusiness.py @@ -0,0 +1,274 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from .common import BaseTest + + +class TestQBusinessApplication(BaseTest): + + def test_application_query(self): + factory = self.replay_flight_data("test_qbusiness_application") + p = self.load_policy( + { + "name": "qbusiness-apps", + "resource": "aws.qbusiness-application", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["applicationArn"]) + self.assertIn("Tags", resources[0]) + + def test_application_cross_account(self): + factory = self.replay_flight_data("test_qbusiness_application_cross_account") + p = self.load_policy( + { + "name": "qbusiness-app-cross-account", + "resource": "aws.qbusiness-application", + "filters": [{"type": "cross-account"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["CrossAccountViolations"]) + + def test_application_tag(self): + factory = self.replay_flight_data("test_qbusiness_application_tag") + client = factory().client("qbusiness") + p = self.load_policy( + { + "name": "qbusiness-app-tag", + "resource": "aws.qbusiness-application", + "actions": [{"type": "tag", "key": "Env", "value": "Test"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["applicationArn"] + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertEqual({t["key"]: t["value"] for t in tags}.get("Env"), "Test") + + p = self.load_policy( + { + "name": "qbusiness-app-untag", + "resource": "aws.qbusiness-application", + "filters": [{"tag:Env": "Test"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertNotIn("Env", [t["key"] for t in tags]) + + +class TestQBusinessIndex(BaseTest): + + def test_index_query(self): + factory = self.replay_flight_data("test_qbusiness_index") + p = self.load_policy( + { + "name": "qbusiness-indices", + "resource": "aws.qbusiness-index", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["indexArn"]) + self.assertTrue(resources[0]["applicationId"]) + + def test_index_tag(self): + factory = self.replay_flight_data("test_qbusiness_index_tag") + client = factory().client("qbusiness") + p = self.load_policy( + { + "name": "qbusiness-index-tag", + "resource": "aws.qbusiness-index", + "actions": [{"type": "tag", "key": "Env", "value": "Test"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["indexArn"] + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertEqual({t["key"]: t["value"] for t in tags}.get("Env"), "Test") + + p = self.load_policy( + { + "name": "qbusiness-index-untag", + "resource": "aws.qbusiness-index", + "filters": [{"tag:Env": "Test"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertNotIn("Env", [t["key"] for t in tags]) + + +class TestQBusinessDataSource(BaseTest): + + def test_data_source_query(self): + factory = self.replay_flight_data("test_qbusiness_data_source") + p = self.load_policy( + { + "name": "qbusiness-data-sources", + "resource": "aws.qbusiness-data-source", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["dataSourceArn"]) + self.assertTrue(resources[0]["applicationId"]) + self.assertTrue(resources[0]["indexId"]) + + def test_data_source_no_vpc(self): + factory = self.replay_flight_data("test_qbusiness_data_source_no_vpc") + p = self.load_policy( + { + "name": "qbusiness-data-source-no-vpc", + "resource": "aws.qbusiness-data-source", + "filters": [ + {"type": "value", + "key": "vpcConfiguration", + "value": "absent"}, + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_data_source_tag(self): + factory = self.replay_flight_data("test_qbusiness_data_source_tag") + client = factory().client("qbusiness") + p = self.load_policy( + { + "name": "qbusiness-data-source-tag", + "resource": "aws.qbusiness-data-source", + "actions": [{"type": "tag", "key": "Env", "value": "Test"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["dataSourceArn"] + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertEqual({t["key"]: t["value"] for t in tags}.get("Env"), "Test") + + p = self.load_policy( + { + "name": "qbusiness-data-source-untag", + "resource": "aws.qbusiness-data-source", + "filters": [{"tag:Env": "Test"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertNotIn("Env", [t["key"] for t in tags]) + + +class TestQBusinessDataAccessor(BaseTest): + + def test_data_accessor_query(self): + factory = self.replay_flight_data("test_qbusiness_data_accessor") + p = self.load_policy( + { + "name": "qbusiness-data-accessors", + "resource": "aws.qbusiness-data-accessor", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["dataAccessorArn"]) + self.assertTrue(resources[0]["principal"]) + + def test_data_accessor_tag(self): + factory = self.replay_flight_data("test_qbusiness_data_accessor_tag") + client = factory().client("qbusiness") + p = self.load_policy( + { + "name": "qbusiness-data-accessor-tag", + "resource": "aws.qbusiness-data-accessor", + "actions": [{"type": "tag", "key": "Env", "value": "Test"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["dataAccessorArn"] + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertEqual({t["key"]: t["value"] for t in tags}.get("Env"), "Test") + + p = self.load_policy( + { + "name": "qbusiness-data-accessor-untag", + "resource": "aws.qbusiness-data-accessor", + "filters": [{"tag:Env": "Test"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertNotIn("Env", [t["key"] for t in tags]) + + +class TestQBusinessPlugin(BaseTest): + + def test_plugin_query(self): + factory = self.replay_flight_data("test_qbusiness_plugin") + p = self.load_policy( + { + "name": "qbusiness-plugins", + "resource": "aws.qbusiness-plugin", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["pluginArn"]) + + def test_plugin_tag(self): + factory = self.replay_flight_data("test_qbusiness_plugin_tag") + client = factory().client("qbusiness") + p = self.load_policy( + { + "name": "qbusiness-plugin-tag", + "resource": "aws.qbusiness-plugin", + "actions": [{"type": "tag", "key": "Env", "value": "Test"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + arn = resources[0]["pluginArn"] + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertEqual({t["key"]: t["value"] for t in tags}.get("Env"), "Test") + + p = self.load_policy( + { + "name": "qbusiness-plugin-untag", + "resource": "aws.qbusiness-plugin", + "filters": [{"tag:Env": "Test"}], + "actions": [{"type": "remove-tag", "tags": ["Env"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + tags = client.list_tags_for_resource(resourceARN=arn)["tags"] + self.assertNotIn("Env", [t["key"] for t in tags]) From 4dd35b62c3c8d13bb276a461fcaa35a013e40232 Mon Sep 17 00:00:00 2001 From: dreakj12 Date: Mon, 28 Sep 2026 13:27:14 -0400 Subject: [PATCH 13/65] azure - metric filter - support parent-scoped dimensions (#11101) --- .../source/azure/examples/metrics-general.rst | 34 + tools/c7n_azure/c7n_azure/filters.py | 79 +- ...ry-cognitiveservice-deployment-metric.json | 690 ++++++++++++++++++ .../tests_azure/templates/provision.sh | 12 +- .../c7n_azure/tests_azure/templates/readme.md | 5 +- .../tests_azure/test_filters_metric.py | 88 +++ .../tests_resources/test_ai_foundry.py | 33 + 7 files changed, 932 insertions(+), 9 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/AiFoundryCognitiveServiceDeploymentTest.ai-foundry-cognitiveservice-deployment-metric.json create mode 100644 tools/c7n_azure/tests_azure/test_filters_metric.py diff --git a/docs/source/azure/examples/metrics-general.rst b/docs/source/azure/examples/metrics-general.rst index 844a8c6440c..58c8249cc04 100644 --- a/docs/source/azure/examples/metrics-general.rst +++ b/docs/source/azure/examples/metrics-general.rst @@ -66,6 +66,40 @@ Find SQL servers with less than 10% average DTU consumption over last 24 hours timeframe: 24 filter: "DatabaseResourceId eq '*'" +The ``dimensions`` key lets a policy filter a resource by a metric of that +resource's parent, restricted back to the resource itself with an OData +filter clause. When ``dimensions`` is set, the filter queries the parent +resource's scope, taken from the ``c7n:parent-id`` annotation, instead of the +resource's own id. Each dimension's ``value`` accepts a literal string, or +one of two sentinels: ``resource-name`` (the resource's own ``name``) or +``resource-id`` (the resource's own ``id``). Because those two strings are +reserved as sentinels, a dimension value can't currently be the literal +string ``resource-name`` or ``resource-id`` themselves. + +Find Azure OpenAI model deployments with no requests in the last week, by +querying the parent Cognitive Services account's ``AzureOpenAIRequests`` +metric and filtering it back to the deployment via the +``ModelDeploymentName`` dimension + +.. code-block:: yaml + + policies: + - name: unused-openai-deployments + resource: azure.cognitiveservice-deployment + filters: + - type: metric + metric: AzureOpenAIRequests + metric_namespace: Microsoft.CognitiveServices/accounts + aggregation: total + op: lte + threshold: 0 + timeframe: 168 + interval: P1D + no_data_action: to_zero + dimensions: + - name: ModelDeploymentName + value: resource-name + Find storage accounts with low blob count .. code-block:: yaml diff --git a/tools/c7n_azure/c7n_azure/filters.py b/tools/c7n_azure/c7n_azure/filters.py index 27ebf97f7f7..16b54114f16 100644 --- a/tools/c7n_azure/c7n_azure/filters.py +++ b/tools/c7n_azure/c7n_azure/filters.py @@ -21,6 +21,7 @@ from c7n_azure.provider import resources from c7n.filters import Filter, FilterValidationError, ValueFilter from c7n.filters.core import PolicyValidationError +from c7n.exceptions import PolicyExecutionError from c7n.filters.metrics import METRIC_WINDOW_ALIGNMENT from c7n.filters.related import RelatedResourceFilter from c7n.filters.offhours import OffHour, OnHour, Time @@ -147,6 +148,42 @@ class MetricFilter(Filter): timeframe: 24 period_start: start-of-day + The ``dimensions`` key lets a policy filter a resource by a metric of that + resource's parent, restricted back to the resource itself with an OData + filter clause. When ``dimensions`` is set, the filter queries the parent + resource's scope, taken from the ``c7n:parent-id`` annotation, instead of + the resource's own id. Each dimension's ``value`` accepts a literal + string, or one of two sentinels: ``resource-name`` (the resource's own + ``name``) or ``resource-id`` (the resource's own ``id``). Because those + two strings are reserved as sentinels, a dimension value can't currently + be the literal string ``resource-name`` or ``resource-id`` themselves. + + :example: + + Find Azure OpenAI model deployments with no requests in the last week, + by querying the parent Cognitive Services account's ``AzureOpenAIRequests`` + metric and filtering it back to the deployment via the + ``ModelDeploymentName`` dimension + + .. code-block:: yaml + + policies: + - name: unused-openai-deployments + resource: azure.cognitiveservice-deployment + filters: + - type: metric + metric: AzureOpenAIRequests + metric_namespace: Microsoft.CognitiveServices/accounts + aggregation: total + op: lte + threshold: 0 + timeframe: 168 + interval: P1D + no_data_action: to_zero + dimensions: + - name: ModelDeploymentName + value: resource-name + """ DEFAULT_TIMEFRAME = 24 @@ -177,7 +214,19 @@ class MetricFilter(Filter): 'PT1M', 'PT5M', 'PT15M', 'PT30M', 'PT1H', 'PT6H', 'PT12H', 'P1D']}, 'aggregation': {'enum': ['total', 'average', 'count', 'minimum', 'maximum']}, 'no_data_action': {'enum': ['include', 'exclude', 'to_zero']}, - 'filter': {'type': 'string'} + 'filter': {'type': 'string'}, + 'dimensions': { + 'type': 'array', + 'items': { + 'type': 'object', + 'additionalProperties': False, + 'required': ['name', 'value'], + 'properties': { + 'name': {'type': 'string'}, + 'value': {'type': 'string'}, + }, + }, + }, } } schema_alias = True @@ -206,6 +255,7 @@ def __init__(self, data, manager=None): self.metricnamespace = self.data.get("metric_namespace", None) # default to false if not passed in self.period_start = self.data.get('period_start', 'auto') + self.dimensions = self.data.get('dimensions', []) def process(self, resources, event=None): # Import utcnow function as it may have been overridden for testing purposes @@ -262,11 +312,33 @@ def get_metric_data(self, resource): return m + _DIMENSION_VALUE_SENTINELS = { + 'resource-name': lambda r: r.get('name'), + 'resource-id': lambda r: r.get('id'), + } + + def resolve_dimension_value(self, resource, value): + resolver = self._DIMENSION_VALUE_SENTINELS.get(value) + return resolver(resource) if resolver else value + def get_resource_id(self, resource): + if self.dimensions and 'c7n:parent-id' in resource: + return resource['c7n:parent-id'] return resource['id'] def get_filter(self, resource): - return self.filter + if not self.dimensions: + return self.filter + clauses = [] + for d in self.dimensions: + value = self.resolve_dimension_value(resource, d['value']) + if value is None: + raise PolicyExecutionError( + "policy:%s Could not resolve dimension %s for resource %s" % ( + self.manager.ctx.policy.name, d['name'], resource.get('id'))) + clauses.append("%s eq '%s'" % (d['name'], str(value).replace("'", "''"))) + dim_filter = " and ".join(clauses) + return "%s and %s" % (self.filter, dim_filter) if self.filter else dim_filter def _write_metric_to_resource(self, resource, metrics_data, m): resource_metrics = resource.setdefault(get_annotation_prefix('metrics'), {}) @@ -276,12 +348,13 @@ def _write_metric_to_resource(self, resource, metrics_data, m): } def _get_metrics_cache_key(self): - return "{}, {}, {}, {}, {}".format( + return "{}, {}, {}, {}, {}, {}".format( self.metric, self.aggregation, self.timeframe, self.interval, self.filter, + self.dimensions, ) def _get_cached_metric_data(self, resource): diff --git a/tools/c7n_azure/tests_azure/cassettes/AiFoundryCognitiveServiceDeploymentTest.ai-foundry-cognitiveservice-deployment-metric.json b/tools/c7n_azure/tests_azure/cassettes/AiFoundryCognitiveServiceDeploymentTest.ai-foundry-cognitiveservice-deployment-metric.json new file mode 100644 index 00000000000..a29d8a7714a --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/AiFoundryCognitiveServiceDeploymentTest.ai-foundry-cognitiveservice-deployment-metric.json @@ -0,0 +1,690 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 6838DD0B86B444919A5282C85806DC0F Ref B: BL2AA2011003031 Ref C: 2026-09-11T19:41:33Z" + ], + "x-envoy-upstream-service-time": [ + "14" + ], + "content-length": [ + "637" + ], + "x-ms-original-request-ids": [ + "1932b8bb-e679-4921-a9dc-d3c9d9dfc4d3" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:32 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [], + "nextLink": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&%24skiptoken=7VE9b8IwFPwvVtuJkA%2fAgkioimiRgshSKEFVFtt5BDfEjmwnkCL%2bew3q1oWhY4cb7t6X3t0ZCTiZJRelRuEZvUar9fsKhWhvTK1D162IIAVUIEyffDUK%2bkxWrm6oZorXhkuhXR%2fjHR6NsQOEUGeYM%2boQmIydgAKbjAKP0hy7tZItz0FpN%2bFMSS13pj%2bTheCGt7AC1XIG2iWMyUYY%2fUxq7rS22x6YBl4wcjzseP7Tgy55vZYliCl0i%2bZju%2ffW6bwjwaaLPyVPB4bF4q21vNnOYkzFRtPZyWqLPa3yw1UD%2f8hperjVLT8lL9HxLsyPfGlB0%2fnwuseiu2vuut%2fO7bZ%2bHgvvcZCjSw8RVUVFoaAgBvLbQ9bz6C1BvVseCVElKCuds59IMhRm%2f6H8MvzPQsnQBV2%2bAQ%3d%3d" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&$skiptoken=7VE9b8IwFPwvVtuJkA/AgkioimiRgshSKEFVFtt5BDfEjmwnkCL%2Bew3q1oWhY4cb7t6X3t0ZCTiZJRelRuEZvUar9fsKhWhvTK1D162IIAVUIEyffDUK%2BkxWrm6oZorXhkuhXR/jHR6NsQOEUGeYM%2BoQmIydgAKbjAKP0hy7tZItz0FpN%2BFMSS13pj%2BTheCGt7AC1XIG2iWMyUYY/Uxq7rS22x6YBl4wcjzseP7Tgy55vZYliCl0i%2BZju/fW6bwjwaaLPyVPB4bF4q21vNnOYkzFRtPZyWqLPa3yw1UD/8hperjVLT8lL9HxLsyPfGlB0/nwuseiu2vuut/O7bZ%2BHgvvcZCjSw8RVUVFoaAgBvLbQ9bz6C1BvVseCVElKCuds59IMhRm/6H8MvzPQsnQBV2%2BAQ%3D%3D", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 0731C7393889434EBA101E6FBB7A2DC4 Ref B: YQ1AA2090602054 Ref C: 2026-09-11T19:41:33Z" + ], + "x-envoy-upstream-service-time": [ + "14" + ], + "content-length": [ + "643" + ], + "x-ms-original-request-ids": [ + "63ff3dd3-5b22-4de5-b2cd-0717fe4f8abd" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:33 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [], + "nextLink": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&%24skiptoken=7VHBTsJAFPyXjXqytBRogIQYUkVLaEwAaWO47G4fZW272%2bxuWyrh313QxIMH%2fQAPc5h5k%2fdeZo6Iw0EvGM8UGh%2fRw3S1flmhMdprXaqxbReY4xQK4LqD3ysJHSoKW1VEUclKzQRXdtfzdt5g6FmAMbH6CSUWhtHQcgnQ0cB1CEk8u5SiZglIZYeMSqHETnd8kXKmWQ0rkDWjoGxMqai4Vne4ZFZt3ObAxHXcgeV4ltO9uVIZK9ciAz6Bdl69xntnHc1a7G7a4E2wqKdpwJe14VXsBx7hG0X8g9Hme1Ik%2bVmDbsNIlF%2fmhrfh%2fbT5E2YNWxiQaNY%2f74n9%2beMyb9gu7iYBd657CTrdfof5zJ9EnphIjYhlMU1TCSnWkFy%2bNwFPlyH69IdYZiCNdNx%2b5b9F4%2b1%2fAz9a%2bbWBLTqh0wc%3d" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&$skiptoken=7VHBTsJAFPyXjXqytBRogIQYUkVLaEwAaWO47G4fZW272%2BxuWyrh313QxIMH/QAPc5h5k/deZo6Iw0EvGM8UGh/Rw3S1flmhMdprXaqxbReY4xQK4LqD3ysJHSoKW1VEUclKzQRXdtfzdt5g6FmAMbH6CSUWhtHQcgnQ0cB1CEk8u5SiZglIZYeMSqHETnd8kXKmWQ0rkDWjoGxMqai4Vne4ZFZt3ObAxHXcgeV4ltO9uVIZK9ciAz6Bdl69xntnHc1a7G7a4E2wqKdpwJe14VXsBx7hG0X8g9Hme1Ik%2BVmDbsNIlF/mhrfh/bT5E2YNWxiQaNY/74n9%2BeMyb9gu7iYBd657CTrdfof5zJ9EnphIjYhlMU1TCSnWkFy%2BNwFPlyH69IdYZiCNdNx%2B5b9F4%2B1/Az9a%2BbWBLTqh0wc%3D", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: C8519699D6544A71BFB6F2B91DDE610F Ref B: YQ1AA2090601034 Ref C: 2026-09-11T19:41:34Z" + ], + "x-envoy-upstream-service-time": [ + "31" + ], + "content-length": [ + "5777" + ], + "x-ms-original-request-ids": [ + "9a46de96-7bb2-4f6b-9ec9-04ec608faa75" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:34 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_cognitive-service-deployment/providers/Microsoft.CognitiveServices/accounts/cctestcogdeploy", + "name": "cctestcogdeploy", + "type": "Microsoft.CognitiveServices/accounts", + "etag": "\"7d002376-0000-0100-0000-6aa455fc0000\"", + "location": "eastus", + "sku": { + "name": "S0" + }, + "kind": "OpenAI", + "properties": { + "endpoint": "https://cctestcogdeploy.openai.azure.com/", + "provisioningState": "Succeeded", + "internalId": "0d074c9ec6c8418fb9cd65d19aed7ab9", + "dateCreated": "2026-09-11T19:00:47.704445Z", + "apiProperties": {}, + "callRateLimit": { + "rules": [ + { + "key": "openai.dalle.post", + "renewalPeriod": 1, + "count": 30, + "matchPatterns": [ + { + "path": "dalle/*", + "method": "POST" + }, + { + "path": "openai/images/*", + "method": "POST" + } + ] + }, + { + "key": "openai.dalle.other", + "renewalPeriod": 1, + "count": 30, + "matchPatterns": [ + { + "path": "dalle/*", + "method": "*" + }, + { + "path": "openai/operations/images/*", + "method": "*" + } + ] + }, + { + "key": "openai.assistants.list", + "renewalPeriod": 60, + "count": 120, + "matchPatterns": [ + { + "path": "openai/assistants", + "method": "GET" + } + ] + }, + { + "key": "openai.threads.list", + "renewalPeriod": 60, + "count": 120, + "matchPatterns": [ + { + "path": "openai/threads", + "method": "GET" + } + ] + }, + { + "key": "openai.vectorstores.list", + "renewalPeriod": 60, + "count": 120, + "matchPatterns": [ + { + "path": "openai/vector_stores", + "method": "GET" + } + ] + }, + { + "key": "openai.vectorstores.post", + "renewalPeriod": 1, + "count": 60, + "matchPatterns": [ + { + "path": "openai/vector_stores", + "method": "POST" + }, + { + "path": "openai/vector_stores/*", + "method": "POST" + } + ] + }, + { + "key": "openai.assistants.default", + "renewalPeriod": 60, + "count": 6000, + "matchPatterns": [ + { + "path": "openai/assistants", + "method": "*" + }, + { + "path": "openai/assistants/*", + "method": "*" + }, + { + "path": "openai/threads", + "method": "*" + }, + { + "path": "openai/threads/*", + "method": "*" + }, + { + "path": "openai/vector_stores", + "method": "*" + }, + { + "path": "openai/vector_stores/*", + "method": "*" + } + ] + }, + { + "key": "openai.responses.default", + "renewalPeriod": 1, + "count": 100000, + "matchPatterns": [ + { + "path": "openai/responses", + "method": "*" + }, + { + "path": "openai/responses/*", + "method": "*" + } + ] + }, + { + "key": "openai.batches.post", + "renewalPeriod": 60, + "count": 30, + "matchPatterns": [ + { + "path": "openai/v1/batches", + "method": "POST" + }, + { + "path": "openai/batches", + "method": "POST" + } + ] + }, + { + "key": "openai.batches.get", + "renewalPeriod": 60, + "count": 500, + "matchPatterns": [ + { + "path": "openai/v1/batches/*", + "method": "GET" + }, + { + "path": "openai/batches/*", + "method": "GET" + } + ] + }, + { + "key": "openai.batches.list", + "renewalPeriod": 60, + "count": 100, + "matchPatterns": [ + { + "path": "openai/v1/batches", + "method": "GET" + }, + { + "path": "openai/batches", + "method": "GET" + } + ] + }, + { + "key": "openai.moderations.post", + "renewalPeriod": 60, + "count": 120, + "matchPatterns": [ + { + "path": "openai/v1/moderations", + "method": "POST" + }, + { + "path": "openai/v1/moderations/exp/m4", + "method": "POST" + }, + { + "path": "openai/v1/moderations/exp/safeberry", + "method": "POST" + }, + { + "path": "openai/v1/safeberry/moderations", + "method": "POST" + } + ] + }, + { + "key": "openai", + "renewalPeriod": 1, + "count": 30, + "matchPatterns": [ + { + "path": "openai/*", + "method": "*" + } + ] + }, + { + "key": "default", + "renewalPeriod": 1, + "count": 30, + "matchPatterns": [ + { + "path": "*", + "method": "*" + } + ] + } + ] + }, + "isMigrated": false, + "customSubDomainName": "cctestcogdeploy", + "allowProjectManagement": false, + "privateEndpointConnections": [], + "publicNetworkAccess": "Enabled", + "capabilities": [ + { + "name": "VirtualNetworks" + }, + { + "name": "CustomerManagedKey" + }, + { + "name": "EnabledApimVnet", + "value": "true" + }, + { + "name": "useEndpointRateLimiter", + "value": "Enabled" + }, + { + "name": "MaxFineTuneCount", + "value": "500" + }, + { + "name": "MaxRunningFineTuneCount", + "value": "3" + }, + { + "name": "MaxUserFileCount", + "value": "100" + }, + { + "name": "MaxTrainingFileSize", + "value": "512000000" + }, + { + "name": "MaxUserFileImportDurationInHours", + "value": "1" + }, + { + "name": "MaxFineTuneJobDurationInHours", + "value": "720" + }, + { + "name": "MaxEvaluationRunDurationInHours", + "value": "5" + }, + { + "name": "MaxRunningEvaluationCount", + "value": "5" + }, + { + "name": "MaxRunningGlobalStandardFineTuneCount", + "value": "3" + }, + { + "name": "TrustedServices", + "value": "Microsoft.CognitiveServices,Microsoft.MachineLearningServices,Microsoft.Search,Microsoft.VideoIndexer" + }, + { + "name": "NetworkSecurityPerimeter" + }, + { + "name": "RaiMonitor" + }, + { + "name": "quotaRefundsEnabled", + "value": "false" + } + ], + "endpoints": { + "OpenAI Language Model Instance API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Dall-E API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Sora API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Moderations API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Whisper API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Model Scaleset API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Realtime API": "https://cctestcogdeploy.openai.azure.com/", + "Token Service API": "https://cctestcogdeploy.openai.azure.com/", + "Azure OpenAI Legacy API - Latest moniker": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Response API on Websocket": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Realtime Translation WebSocket API": "https://cctestcogdeploy.openai.azure.com/", + "OpenAI Realtime Live WebSocket API": "https://cctestcogdeploy.openai.azure.com/" + } + }, + "systemData": { + "createdBy": "test@example.com", + "createdByType": "User", + "createdAt": "2026-09-11T19:00:47.0950693Z", + "lastModifiedBy": "test@example.com", + "lastModifiedByType": "User", + "lastModifiedAt": "2026-09-11T19:26:17.286859Z" + } + } + ], + "nextLink": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&%24skiptoken=7VHLTsJAFP2Xibqy9AE00IQYgqIlNiaAQEw387iUse1MMzMtr%2fDvDmjiwoV%2bgIuzOOee3HtzzhEJ2JlnLnKNoiN6GM7mrzMUoY0xlY5ct8QCZ1CCMC18qBW0qCxdXRNNFa8Ml0K7fhiuw24vdABj4nQYJQ6Gfs8JCNB%2bN%2fAIYaFbKdlwBkq7CadKark2rZHMBDe8gRmohlPQLqZU1sLoO1xxp7Fue2AQeEHX8ULH82%2budM6rucxBDGA%2fqd9WG2%2b%2bHO9xsNjH75Iv24bGYtpYXq9GcUjEQpPRzmqTDSlZcdbA33KyLC5zyw%2fJ%2fXD7J4y3%2fNmCLMed857VaPI4LbZ8vfJZLLzrNkOn2%2b8wX8STLJiN1IpYlcMsU5BhA%2bzyvQ14OE3Qpz%2fBKgdlpWP6lX%2bKovS%2fgR%2bt%2fNpAik7o9AE%3d" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.CognitiveServices/accounts?api-version=2025-06-01&$skiptoken=7VHLTsJAFP2Xibqy9AE00IQYgqIlNiaAQEw387iUse1MMzMtr/DvDmjiwoV%2BgIuzOOee3HtzzhEJ2JlnLnKNoiN6GM7mrzMUoY0xlY5ct8QCZ1CCMC18qBW0qCxdXRNNFa8Ml0K7fhiuw24vdABj4nQYJQ6Gfs8JCNB%2BN/AIYaFbKdlwBkq7CadKark2rZHMBDe8gRmohlPQLqZU1sLoO1xxp7Fue2AQeEHX8ULH82%2BudM6rucxBDGA/qd9WG2%2B%2BHO9xsNjH75Iv24bGYtpYXq9GcUjEQpPRzmqTDSlZcdbA33KyLC5zyw/J/XD7J4y3/NmCLMed857VaPI4LbZ8vfJZLLzrNkOn2%2B8wX8STLJiN1IpYlcMsU5BhA%2BzyvQ14OE3Qpz/BKgdlpWP6lX%2BKovS/gR%2Bt/NpAik7o9AE%3D", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: A206CE8DCF314CD68AD40276CBDB2DDE Ref B: MNZ221060610047 Ref C: 2026-09-11T19:41:35Z" + ], + "x-envoy-upstream-service-time": [ + "19" + ], + "content-length": [ + "12" + ], + "x-ms-original-request-ids": [ + "788590c8-1484-43c0-add0-9ab5e4b91fba" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:35 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com//subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_cognitive-service-deployment/providers/Microsoft.CognitiveServices/accounts/cctestcogdeploy/deployments?api-version=2024-10-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 721138783B054B14BCA5B39B894D74F2 Ref B: YQ1AA2090601062 Ref C: 2026-09-11T19:41:36Z" + ], + "x-envoy-upstream-service-time": [ + "32" + ], + "content-length": [ + "1088" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:35 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_cognitive-service-deployment/providers/Microsoft.CognitiveServices/accounts/cctestcogdeploy/deployments/cctest-gpt4o-mini", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "name": "cctest-gpt4o-mini", + "sku": { + "name": "GlobalStandard", + "capacity": 1 + }, + "properties": { + "model": { + "format": "OpenAI", + "name": "gpt-5-nano", + "version": "2025-08-07" + }, + "versionUpgradeOption": "OnceNewDefaultVersionAvailable", + "currentCapacity": 1, + "capabilities": { + "area": "US", + "chatCompletion": "true", + "responses": "true", + "agentsV2": "true", + "assistants": "true" + }, + "raiPolicyName": "Microsoft.DefaultV2", + "provisioningState": "Succeeded", + "rateLimits": [ + { + "key": "request", + "renewalPeriod": 60, + "count": 1 + }, + { + "key": "token", + "renewalPeriod": 60, + "count": 1000 + } + ] + }, + "systemData": { + "createdBy": "test@example.com", + "createdByType": "User", + "createdAt": "2026-09-11T19:26:50.364454Z", + "lastModifiedBy": "test@example.com", + "lastModifiedByType": "User", + "lastModifiedAt": "2026-09-11T19:26:50.364454Z" + }, + "etag": "\"f3938b46-af9b-44f8-b7a3-261f5e435935\"" + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_cognitive-service-deployment/providers/Microsoft.CognitiveServices/accounts/cctestcogdeploy/providers/microsoft.insights/metrics?timespan=2026-09-04%2023%3A59%3A59%2F2026-09-11%2023%3A59%3A59&interval=P1D&metricnames=AzureOpenAIRequests&aggregation=total&$filter=ModelDeploymentName%20eq%20%27cctest-gpt4o-mini%27&api-version=2018-01-01&metricnamespace=Microsoft.CognitiveServices%2Faccounts", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 7BF3C6AE8DC243398E8C0D7FA787B50B Ref B: MNZ221060618045 Ref C: 2026-09-11T19:41:37Z" + ], + "x-envoy-upstream-service-time": [ + "51" + ], + "mise-correlation-id": [ + "d7936be2-923a-4f65-b41a-3c490be6bbd3" + ], + "content-length": [ + "1034" + ], + "date": [ + "Fri, 11 Sep 2026 19:41:37 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-resource-requests": [ + "999" + ] + }, + "body": { + "data": { + "cost": 10079, + "timespan": "2026-09-04T23:59:59Z/2026-09-11T23:59:59Z", + "interval": "P1D", + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_cognitive-service-deployment/providers/Microsoft.CognitiveServices/accounts/cctestcogdeploy/providers/Microsoft.Insights/metrics/AzureOpenAIRequests", + "type": "Microsoft.Insights/metrics", + "name": { + "value": "AzureOpenAIRequests", + "localizedValue": "Azure OpenAI Requests" + }, + "displayDescription": "Number of calls made to the Azure OpenAI API over a period of time. Applies to PTU, PTU-Managed and Pay-as-you-go deployments. To breakdown API requests, you can add a filter or apply splitting by the following dimensions: ModelDeploymentName, ModelName, ModelVersion, StatusCode (successful, clienterrors, server errors), IsSpillover for spillover information, ServiceTier, StreamType (Streaming vs non-streaming requests) and operation.", + "unit": "Count", + "timeseries": [], + "errorCode": "Success" + } + ], + "namespace": "Microsoft.CognitiveServices/accounts", + "resourceregion": "eastus" + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/provision.sh b/tools/c7n_azure/tests_azure/templates/provision.sh index 2c06ae39df3..21953fa9c03 100755 --- a/tools/c7n_azure/tests_azure/templates/provision.sh +++ b/tools/c7n_azure/tests_azure/templates/provision.sh @@ -189,10 +189,12 @@ deploy_resource() { elif [[ "$fileName" == "cognitive-service-deployment.json" ]]; then openai_location="${AZURE_OPENAI_LOCATION:-eastus}" - model_name="${AZURE_OPENAI_MODEL_NAME:-gpt-4o-mini}" - model_version="${AZURE_OPENAI_MODEL_VERSION:-2024-07-18}" + model_name="${AZURE_OPENAI_MODEL_NAME:-gpt-5-nano}" + model_version="${AZURE_OPENAI_MODEL_VERSION:-2025-08-07}" + # Pinned by committed test fixtures (recorded VCR cassettes reference this exact + # deployment name) -- do not rename to match the model default above. deployment_name="${AZURE_OPENAI_DEPLOYMENT_NAME:-cctest-gpt4o-mini}" - account_name="${AZURE_OPENAI_ACCOUNT_NAME}" + account_name="${AZURE_OPENAI_ACCOUNT_NAME:-cctestcogdeploy}" if az cognitiveservices account show-deleted \ --resource-group "$rgName" \ @@ -249,6 +251,8 @@ deploy_resource() { exit 1 fi + sku_name="${AZURE_OPENAI_SKU_NAME:-GlobalStandard}" + if ! az cognitiveservices account deployment create \ --resource-group $rgName \ --name $account_name \ @@ -256,7 +260,7 @@ deploy_resource() { --model-format OpenAI \ --model-name $model_name \ --model-version $model_version \ - --sku-name Standard \ + --sku-name $sku_name \ --sku-capacity 1 \ --output None; then echo "Failed to create Cognitive Services deployment ${deployment_name} in account ${account_name}" diff --git a/tools/c7n_azure/tests_azure/templates/readme.md b/tools/c7n_azure/tests_azure/templates/readme.md index 73a7be6d4a8..156dcd0f093 100644 --- a/tools/c7n_azure/tests_azure/templates/readme.md +++ b/tools/c7n_azure/tests_azure/templates/readme.md @@ -30,8 +30,9 @@ If you deploy `cognitive-service-deployment`, provisioning creates an OpenAI acc - `AZURE_OPENAI_ACCOUNT_NAME` (default: `cctestcogdeploy`) - `AZURE_OPENAI_LOCATION` (default: `eastus`) -- `AZURE_OPENAI_MODEL_NAME` (default: `gpt-4o-mini`) -- `AZURE_OPENAI_MODEL_VERSION` (default: `2024-07-18`) +- `AZURE_OPENAI_MODEL_NAME` (default: `gpt-5-nano`) +- `AZURE_OPENAI_MODEL_VERSION` (default: `2025-08-07`) - `AZURE_OPENAI_DEPLOYMENT_NAME` (default: `cctest-gpt4o-mini`) +- `AZURE_OPENAI_SKU_NAME` (default: `GlobalStandard`) — must be a SKU the chosen model/version actually supports; check with `az cognitiveservices account list-models --resource-group --name -o json` (the `skus` field per model) if changing the model. If a soft-deleted Cognitive Services account with the target name already exists, provisioning will purge it automatically before redeploying. diff --git a/tools/c7n_azure/tests_azure/test_filters_metric.py b/tools/c7n_azure/tests_azure/test_filters_metric.py new file mode 100644 index 00000000000..c58120c4388 --- /dev/null +++ b/tools/c7n_azure/tests_azure/test_filters_metric.py @@ -0,0 +1,88 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from unittest.mock import Mock + +from .azure_common import BaseTest +from c7n_azure.filters import MetricFilter + + +class MetricFilterDimensionsTest(BaseTest): + + def test_schema_accepts_dimensions(self): + p = self.load_policy({ + 'name': 'test-metric-dimensions', + 'resource': 'azure.vm', + 'filters': [ + {'type': 'metric', + 'metric': 'AzureOpenAIRequests', + 'metric_namespace': 'Microsoft.CognitiveServices/accounts', + 'op': 'lte', + 'threshold': 0, + 'dimensions': [ + {'name': 'ModelDeploymentName', 'value': 'resource-name'}]}] + }, validate=True) + self.assertTrue(p) + + def _get_filter(self, dimensions=None): + data = {'type': 'metric', 'metric': 'AzureOpenAIRequests', 'op': 'lte', 'threshold': 0} + if dimensions is not None: + data['dimensions'] = dimensions + return MetricFilter(data=data, manager=Mock()) + + def test_resolve_dimension_value_literal(self): + f = self._get_filter() + self.assertEqual(f.resolve_dimension_value({'name': 'anything'}, 'a-literal-value'), + 'a-literal-value') + + def test_resolve_dimension_value_resource_name(self): + f = self._get_filter() + self.assertEqual( + f.resolve_dimension_value({'name': 'my-deployment', 'id': 'ignored'}, + 'resource-name'), + 'my-deployment') + + def test_resolve_dimension_value_resource_id(self): + f = self._get_filter() + self.assertEqual( + f.resolve_dimension_value({'name': 'ignored', 'id': '/subscriptions/x'}, + 'resource-id'), + '/subscriptions/x') + + def test_get_resource_id_without_dimensions_uses_own_id(self): + f = self._get_filter() + resource = {'id': '/subscriptions/child', 'c7n:parent-id': '/subscriptions/parent'} + self.assertEqual(f.get_resource_id(resource), '/subscriptions/child') + + def test_get_resource_id_with_dimensions_and_parent_id_uses_parent(self): + f = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'resource-name'}]) + resource = {'id': '/subscriptions/child', 'c7n:parent-id': '/subscriptions/parent', + 'name': 'my-deployment'} + self.assertEqual(f.get_resource_id(resource), '/subscriptions/parent') + + def test_get_resource_id_with_dimensions_but_no_parent_id_falls_back(self): + f = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'resource-name'}]) + resource = {'id': '/subscriptions/child', 'name': 'my-deployment'} + self.assertEqual(f.get_resource_id(resource), '/subscriptions/child') + + def test_get_filter_without_dimensions_returns_plain_filter(self): + f = self._get_filter() + f.filter = "DatabaseName eq 'x'" + self.assertEqual(f.get_filter({'id': 'x'}), "DatabaseName eq 'x'") + + def test_get_filter_with_dimensions_builds_odata_clause(self): + f = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'resource-name'}]) + resource = {'id': '/subscriptions/child', 'name': 'my-deployment'} + self.assertEqual(f.get_filter(resource), "ModelDeploymentName eq 'my-deployment'") + + def test_get_filter_with_dimensions_and_existing_filter_ands_together(self): + f = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'resource-name'}]) + f.filter = "Region eq 'eastus'" + resource = {'id': '/subscriptions/child', 'name': 'my-deployment'} + self.assertEqual( + f.get_filter(resource), + "Region eq 'eastus' and ModelDeploymentName eq 'my-deployment'") + + def test_get_metrics_cache_key_differs_by_dimensions(self): + f1 = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'dep-a'}]) + f2 = self._get_filter(dimensions=[{'name': 'ModelDeploymentName', 'value': 'dep-b'}]) + self.assertNotEqual(f1._get_metrics_cache_key(), f2._get_metrics_cache_key()) diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_ai_foundry.py b/tools/c7n_azure/tests_azure/tests_resources/test_ai_foundry.py index 5e5c7d86b83..928ac47dd28 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_ai_foundry.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_ai_foundry.py @@ -135,6 +135,39 @@ def test_cognitiveservice_deployment_get_resources(self): self.assertGreaterEqual(len(resources), 1) self.assertTrue(any(r['name'].endswith(deployment_name) for r in resources)) + @requires_arm_polling + @arm_template('cognitive-service-deployment.json') + @cassette_name('ai-foundry-cognitiveservice-deployment-metric') + def test_cognitiveservice_deployment_metric_filter(self): + deployment_name = os.environ.get('AZURE_OPENAI_DEPLOYMENT_NAME', 'cctest-gpt4o-mini') + p = self.load_policy( + { + 'name': 'test-azure-cognitiveservice-deployment-metric', + 'resource': 'azure.cognitiveservice-deployment', + 'filters': [ + {'type': 'value', + 'key': 'name', + 'op': 'glob', + 'value': '*' + deployment_name}, + {'type': 'metric', + 'metric': 'AzureOpenAIRequests', + 'metric_namespace': 'Microsoft.CognitiveServices/accounts', + 'aggregation': 'total', + 'op': 'lte', + 'threshold': 0, + 'timeframe': 168, + 'interval': 'P1D', + 'no_data_action': 'to_zero', + 'dimensions': [ + {'name': 'ModelDeploymentName', 'value': 'resource-name'}]}], + }, + validate=True, + ) + + resources = p.run() + + self.assertEqual(len(resources), 1) + def test_cognitiveservice_deployment_tagging_not_implemented(self): with self.assertRaises(PolicyValidationError): self.load_policy( From be9983ac56a13ed55dd98f6804883ed18b5ba5fa Mon Sep 17 00:00:00 2001 From: Matthew Heidelbaugh <42586165+mattheidelbaugh@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:26:31 -0400 Subject: [PATCH 14/65] aws - expand iot support (#11098) - iot-policy: parses policyDocument in augment, has-statement and attached filters, delete action - iot-certificate: describe_certificate detail with creationDate for age based filtering, set-inactive action - iot-ota-update: unsigned filter over otaUpdateFiles codeSigning - iot: tag support for ownership attribution - account: iot-logging filter for GetV2LoggingOptions --- c7n/resources/account.py | 54 +++ c7n/resources/iot.py | 324 +++++++++++++++++- c7n/resources/resource_map.py | 3 + .../iot.DescribeCertificate_1.json | 59 ++++ .../iot.ListCertificates_1.json | 23 ++ .../iot.DescribeCertificate_1.json | 59 ++++ .../iot.ListCertificates_1.json | 23 ++ .../iot.UpdateCertificate_1.json | 6 + .../iam.ListAccountAliases_1.json | 10 + .../iot.GetV2LoggingOptions_1.json | 19 + .../iam.ListAccountAliases_1.json | 10 + .../iot.GetV2LoggingOptions_1.json | 19 + .../iam.ListAccountAliases_1.json | 10 + .../iot.GetV2LoggingOptions_1.json | 11 + .../iot.GetOTAUpdate_1.json | 52 +++ .../iot.GetOTAUpdate_2.json | 52 +++ .../iot.ListOTAUpdates_1.json | 22 ++ .../iot.ListOTAUpdates_2.json | 22 ++ .../iot.ListTagsForResource_1.json | 7 + .../iot.ListTagsForResource_2.json | 12 + .../iot.ListTagsForResource_3.json | 12 + .../iot.ListTagsForResource_4.json | 7 + .../iot.TagResource_1.json | 6 + .../iot.UntagResource_1.json | 6 + .../iot.GetOTAUpdate_1.json | 56 +++ .../iot.ListOTAUpdates_1.json | 22 ++ .../iot.ListTagsForResource_1.json | 7 + .../iot.DeletePolicy_1.json | 6 + .../iot.GetPolicy_1.json | 31 ++ .../iot.ListPolicies_1.json | 12 + .../iot.ListPolicies_2.json | 7 + .../iot.ListPolicyVersions_1.json | 22 ++ .../iot.ListTagsForResource_1.json | 7 + .../iot.ListTargetsForPolicy_1.json | 7 + .../iot.GetPolicy_1.json | 31 ++ .../iot.GetPolicy_2.json | 31 ++ .../iot.ListPolicies_1.json | 12 + .../iot.ListPolicies_2.json | 12 + .../iot.ListTagsForResource_1.json | 7 + .../iot.ListTagsForResource_2.json | 12 + .../iot.ListTagsForResource_3.json | 12 + .../iot.ListTagsForResource_4.json | 7 + .../iot.TagResource_1.json | 6 + .../iot.UntagResource_1.json | 6 + .../iot.GetPolicy_1.json | 31 ++ .../iot.ListPolicies_1.json | 12 + .../iot.ListTagsForResource_1.json | 12 + .../iot.ListTargetsForPolicy_1.json | 7 + .../iot.GetPolicy_1.json | 31 ++ .../iot.GetPolicy_2.json | 31 ++ .../iot.ListPolicies_1.json | 16 + .../iot.ListTagsForResource_1.json | 7 + .../iot.ListTagsForResource_2.json | 7 + .../iot.ListThings_1.json | 17 + tests/test_iot.py | 289 ++++++++++++++++ 55 files changed, 1599 insertions(+), 2 deletions(-) create mode 100644 tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json create mode 100644 tests/data/placebo/test_iot_certificate_age/iot.ListCertificates_1.json create mode 100644 tests/data/placebo/test_iot_certificate_set_inactive/iot.DescribeCertificate_1.json create mode 100644 tests/data/placebo/test_iot_certificate_set_inactive/iot.ListCertificates_1.json create mode 100644 tests/data/placebo/test_iot_certificate_set_inactive/iot.UpdateCertificate_1.json create mode 100644 tests/data/placebo/test_iot_logging_configured_disabled/iam.ListAccountAliases_1.json create mode 100644 tests/data/placebo/test_iot_logging_configured_disabled/iot.GetV2LoggingOptions_1.json create mode 100644 tests/data/placebo/test_iot_logging_enabled/iam.ListAccountAliases_1.json create mode 100644 tests/data/placebo/test_iot_logging_enabled/iot.GetV2LoggingOptions_1.json create mode 100644 tests/data/placebo/test_iot_logging_not_configured/iam.ListAccountAliases_1.json create mode 100644 tests/data/placebo/test_iot_logging_not_configured/iot.GetV2LoggingOptions_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_2.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_2.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.TagResource_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_tag_untag/iot.UntagResource_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_unsigned/iot.GetOTAUpdate_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_unsigned/iot.ListOTAUpdates_1.json create mode 100644 tests/data/placebo/test_iot_ota_update_unsigned/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.DeletePolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.GetPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_2.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.ListPolicyVersions_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_delete/iot.ListTargetsForPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_2.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_1.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_2.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_3.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_4.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.TagResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_tag_untag/iot.UntagResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_unattached/iot.GetPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_unattached/iot.ListPolicies_1.json create mode 100644 tests/data/placebo/test_iot_policy_unattached/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_unattached/iot.ListTargetsForPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_1.json create mode 100644 tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_2.json create mode 100644 tests/data/placebo/test_iot_policy_wildcard/iot.ListPolicies_1.json create mode 100644 tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_1.json create mode 100644 tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_2.json create mode 100644 tests/data/placebo/test_iot_thing_query/iot.ListThings_1.json create mode 100644 tests/test_iot.py diff --git a/c7n/resources/account.py b/c7n/resources/account.py index b924b3dadea..1937b6711de 100644 --- a/c7n/resources/account.py +++ b/c7n/resources/account.py @@ -2544,6 +2544,60 @@ def get_item_values(self, resource): return item_values +@filters.register('iot-logging') +class IoTLogging(ValueFilter): + """Check account level IoT logging configuration + + IoT device activity logs are delivered to CloudWatch Logs. On top of the + fields returned by GetV2LoggingOptions, an extra key, loggingConfigured, + is set to true or false to signify whether logging has been configured for + the account at all. No other fields are present when it is false, so match + on loggingConfigured rather than on a missing field. + + :example: + + .. code-block:: yaml + + policies: + - name: iot-logging-not-enabled + resource: account + filters: + - or: + - type: iot-logging + key: loggingConfigured + value: false + - type: iot-logging + key: disableAllLogs + value: true + - type: iot-logging + key: defaultLogLevel + value: DISABLED + """ + annotation_key = 'c7n:IoTLogging' + annotate = False + schema = type_schema('iot-logging', rinherit=ValueFilter.schema) + schema_alias = False + permissions = ('iot:GetV2LoggingOptions',) + + def process(self, resources, event=None): + self.augment([r for r in resources if self.annotation_key not in r]) + return super().process(resources, event) + + def augment(self, resources): + client = local_session(self.manager.session_factory).client('iot') + for r in resources: + try: + options = client.get_v2_logging_options() + options.pop('ResponseMetadata', None) + options['loggingConfigured'] = True + except client.exceptions.NotConfiguredException: + options = {'loggingConfigured': False} + r[self.annotation_key] = options + + def __call__(self, r): + return super().__call__(r[self.annotation_key]) + + @actions.register('set-bedrock-model-invocation-logging') class SetBedrockModelInvocationLogging(BaseAction): """Set Bedrock Model Invocation Logging Configuration on an account. diff --git a/c7n/resources/iot.py b/c7n/resources/iot.py index 033041403d0..8308e589e0f 100644 --- a/c7n/resources/iot.py +++ b/c7n/resources/iot.py @@ -1,8 +1,70 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 - -from c7n.query import QueryResourceManager, TypeInfo +from c7n.actions import Action +from c7n.filters import Filter +from c7n.filters.policystatement import HasStatementFilter from c7n.manager import resources +from c7n.query import ( + DescribeSource, QueryResourceManager, RetryPageIterator, TypeInfo) +from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction +from c7n.utils import get_retry, local_session, type_schema + + +RETRY = get_retry(( + 'ThrottlingException', + 'ServiceUnavailableException', + 'InternalFailureException', +)) + + +class TagIoTResource(Tag): + + permissions = ('iot:TagResource',) + + def process_resource_set(self, client, resources, new_tags): + arn_key = self.manager.resource_type.arn + for r in resources: + try: + self.manager.retry( + client.tag_resource, resourceArn=r[arn_key], tags=new_tags) + except client.exceptions.ResourceNotFoundException: + continue + + +class RemoveTagIoTResource(RemoveTag): + + permissions = ('iot:UntagResource',) + + def process_resource_set(self, client, resources, tag_keys): + arn_key = self.manager.resource_type.arn + for r in resources: + try: + self.manager.retry( + client.untag_resource, + resourceArn=r[arn_key], tagKeys=tag_keys) + except client.exceptions.ResourceNotFoundException: + continue + + +def register_iot_tagging(klass): + klass.action_registry.register('tag', TagIoTResource) + klass.action_registry.register('remove-tag', RemoveTagIoTResource) + klass.action_registry.register('mark-for-op', TagDelayedAction) + klass.filter_registry.register('marked-for-op', TagActionFilter) + return klass + + +class DescribeIoTResource(DescribeSource): + + def augment(self, resources): + resources = super().augment(resources) + client = local_session(self.manager.session_factory).client('iot') + arn_key = self.manager.resource_type.arn + for r in resources: + r['Tags'] = self.manager.retry( + client.list_tags_for_resource, + resourceArn=r[arn_key]).get('tags', []) + return resources @resources.register('iot') @@ -19,3 +81,261 @@ class resource_type(TypeInfo): 'thingTypeName' ) cfn_type = 'AWS::IoT::Thing' + + retry = staticmethod(RETRY) + + +@register_iot_tagging +@resources.register('iot-policy') +class IoTPolicy(QueryResourceManager): + """AWS IoT policy.""" + + class resource_type(TypeInfo): + service = 'iot' + enum_spec = ('list_policies', 'policies', None) + detail_spec = ('get_policy', 'policyName', 'policyName', None) + id = 'policyName' + name = 'policyName' + arn = 'policyArn' + cfn_type = 'AWS::IoT::Policy' + permissions_augment = ('iot:ListTagsForResource',) + + source_mapping = {'describe': DescribeIoTResource} + retry = staticmethod(RETRY) + + +@IoTPolicy.filter_registry.register('attached') +class IoTPolicyAttached(Filter): + """Filter IoT policies by whether they are attached to any target. + + :example: + + .. code-block:: yaml + + policies: + - name: iot-policy-orphaned + resource: aws.iot-policy + filters: + - type: attached + state: false + """ + + schema = type_schema('attached', state={'type': 'boolean'}) + permissions = ('iot:ListTargetsForPolicy',) + annotation_key = 'c7n:Targets' + + def process(self, resources, event=None): + client = local_session(self.manager.session_factory).client('iot') + pager = client.get_paginator('list_targets_for_policy') + pager.PAGE_ITERATOR_CLS = RetryPageIterator + for r in resources: + r[self.annotation_key] = pager.paginate( + policyName=r['policyName']).build_full_result().get('targets', []) + state = self.data.get('state', True) + return [r for r in resources + if bool(r[self.annotation_key]) == state] + + +@IoTPolicy.filter_registry.register('has-statement') +class IoTPolicyHasStatement(HasStatementFilter): + + policy_attribute = 'policyDocument' + + def get_std_format_args(self, policy): + return { + 'policy_arn': policy['policyArn'], + 'account_id': self.manager.config.account_id, + 'region': self.manager.config.region, + } + + +@IoTPolicy.action_registry.register('delete') +class DeleteIoTPolicy(Action): + """Delete an IoT policy. + + Non-default versions are deleted and targets detached first, as required + by the API. Set ``force`` to detach targets; without it an attached policy + is skipped. + + :example: + + .. code-block:: yaml + + policies: + - name: iot-policy-delete-orphaned + resource: aws.iot-policy + filters: + - type: attached + state: false + actions: + - delete + """ + + schema = type_schema('delete', force={'type': 'boolean'}) + permissions = ( + 'iot:DeletePolicy', 'iot:DeletePolicyVersion', + 'iot:ListPolicyVersions', 'iot:ListTargetsForPolicy', + 'iot:DetachPolicy') + + def process(self, resources): + client = local_session(self.manager.session_factory).client('iot') + force = self.data.get('force', False) + for r in resources: + try: + if not self._detach(client, r, force): + continue + self._delete_versions(client, r) + self.manager.retry( + client.delete_policy, policyName=r['policyName']) + except client.exceptions.ResourceNotFoundException: + continue + except client.exceptions.DeleteConflictException as e: + self.log.warning( + 'policy:%s could not be deleted, it may take up to five ' + 'minutes after detachment before deletion succeeds: %s', + r['policyName'], e) + + def _detach(self, client, r, force): + targets = r.get(IoTPolicyAttached.annotation_key) + if targets is None: + pager = client.get_paginator('list_targets_for_policy') + pager.PAGE_ITERATOR_CLS = RetryPageIterator + targets = pager.paginate( + policyName=r['policyName']).build_full_result().get('targets', []) + if targets and not force: + self.log.warning( + 'policy:%s detachment skipped, attached to %d target(s). ' + 'Use "force" flag to detach.', + r['policyName'], len(targets)) + return False + for t in targets: + self.manager.retry( + client.detach_policy, policyName=r['policyName'], target=t) + return True + + def _delete_versions(self, client, r): + versions = self.manager.retry( + client.list_policy_versions, policyName=r['policyName']) + for v in versions.get('policyVersions', []): + if v['isDefaultVersion']: + continue + self.manager.retry( + client.delete_policy_version, + policyName=r['policyName'], policyVersionId=v['versionId']) + + +@resources.register('iot-certificate') +class IoTCertificate(QueryResourceManager): + """AWS IoT device X.509 certificate.""" + + class resource_type(TypeInfo): + service = 'iot' + enum_spec = ('list_certificates', 'certificates', None) + detail_spec = ( + 'describe_certificate', 'certificateId', + 'certificateId', 'certificateDescription') + id = 'certificateId' + name = 'certificateId' + arn = 'certificateArn' + date = 'lastModifiedDate' + cfn_type = 'AWS::IoT::Certificate' + + retry = staticmethod(RETRY) + + +@IoTCertificate.action_registry.register('set-inactive') +class SetCertificateInactive(Action): + """Deactivate an IoT device certificate. + + :example: + + .. code-block:: yaml + + policies: + - name: iot-cert-rotate-12-months + resource: aws.iot-certificate + filters: + - type: value + key: status + value: ACTIVE + - type: value + key: creationDate + value_type: age + op: greater-than + value: 365 + actions: + - set-inactive + """ + + schema = type_schema('set-inactive') + permissions = ('iot:UpdateCertificate',) + + def process(self, resources): + client = local_session(self.manager.session_factory).client('iot') + for r in resources: + self.manager.retry( + client.update_certificate, + certificateId=r['certificateId'], newStatus='INACTIVE') + + +class DescribeIoTOTAUpdate(DescribeIoTResource): + + def get_permissions(self): + perms = super().get_permissions() + perms.remove('iot:GetOtaUpdate') + perms.append('iot:GetOTAUpdate') + return perms + + +@register_iot_tagging +@resources.register('iot-ota-update') +class IoTOTAUpdate(QueryResourceManager): + """AWS IoT Over-the-Air (OTA) update.""" + + class resource_type(TypeInfo): + service = 'iot' + enum_spec = ('list_ota_updates', 'otaUpdates', None) + detail_spec = ( + 'get_ota_update', 'otaUpdateId', 'otaUpdateId', 'otaUpdateInfo') + id = 'otaUpdateId' + name = 'otaUpdateId' + arn = 'otaUpdateArn' + date = 'lastModifiedDate' + permissions_enum = ('iot:ListOTAUpdates',) + permissions_augment = ('iot:ListTagsForResource',) + + source_mapping = {'describe': DescribeIoTOTAUpdate} + retry = staticmethod(RETRY) + + +@IoTOTAUpdate.filter_registry.register('unsigned') +class IoTOTAUpdateUnsigned(Filter): + """Select OTA updates that include an unsigned file. + + :example: + + .. code-block:: yaml + + policies: + - name: iot-ota-unsigned + resource: aws.iot-ota-update + filters: + - type: unsigned + """ + + schema = type_schema('unsigned') + permissions = ('iot:GetOTAUpdate',) + + def process(self, resources, event=None): + return [r for r in resources if self._has_unsigned_file(r)] + + def _has_unsigned_file(self, r): + for f in r.get('otaUpdateFiles', []): + signing = f.get('codeSigning', {}) + signed = ( + signing.get('awsSignerJobId') + or signing.get('startSigningJobParameter') + or signing.get('customCodeSigning')) + if not signed: + return True + return False diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index f36dc1eb4d0..ab77b6c3e24 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -209,6 +209,9 @@ "aws.inspector2-finding": "c7n.resources.inspector2.Inspector2Finding", "aws.internet-gateway": "c7n.resources.vpc.InternetGateway", "aws.iot": "c7n.resources.iot.IoT", + "aws.iot-certificate": "c7n.resources.iot.IoTCertificate", + "aws.iot-ota-update": "c7n.resources.iot.IoTOTAUpdate", + "aws.iot-policy": "c7n.resources.iot.IoTPolicy", "aws.kafka": "c7n.resources.kafka.Kafka", "aws.kafka-config": "c7n.resources.kafka.KafkaClusterConfiguration", "aws.kendra": "c7n.resources.kendra.KendraIndex", diff --git a/tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json b/tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json new file mode 100644 index 00000000000..54ab533f2a9 --- /dev/null +++ b/tests/data/placebo/test_iot_certificate_age/iot.DescribeCertificate_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "certificateDescription": { + "certificateArn": "arn:aws:iot:us-east-1:644160558196:cert/6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "certificateId": "6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "status": "ACTIVE", + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIDWTCCAkGgAwIBAgIUWFTN5o/slZ/2twBgyviexrtCgD0wDQYJKoZIhvcNAQEL\nBQAwTTFLMEkGA1UECwxCQW1hem9uIFdlYiBTZXJ2aWNlcyBPPUFtYXpvbi5jb20g\nSW5jLiBMPVNlYXR0bGUgU1Q9V2FzaGluZ3RvbiBDPVVTMB4XDTI2MDkwOTIwMTIw\nOFoXDTQ5MTIzMTIzNTk1OVowHjEcMBoGA1UEAwwTQVdTIElvVCBDZXJ0aWZpY2F0\nZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALC7RWgH/gYXudBiZhha\nOUmQiLbsi00NrXBCSNgi0j1Knb3BNr2ERifuph+FWmHE0GTFlwnhkJdxW+GDUxf4\n3ZpNPZvFD9LzXZiBz2b2z6NXhtqhShzP4TEcG2yOZc2+zmGTDs6psPBW2erkFQC3\nFcMEn3mYE8/gzsrvgR7kW5xYZstB9ahCykrCSkOH1ZQ2TtGI84Kuj81xIZ7OoSxS\nxT3uIZVtVeyhf2hjEsMof9syLjlUd6C0guafepXXS1WncdGfoeD1cc8ZbUi/MiVJ\nqJDNQ8jBMeDvVP3cb8Gyx3ye016xKK9T6DTj9/f36RGKKFo98nmQdDKoQXbtHV4D\nhbECAwEAAaNgMF4wHwYDVR0jBBgwFoAUaJ/QZNsnbsmF0cLGMsn5JgI9G9cwHQYD\nVR0OBBYEFGt3RQsx2JO1fLnKqLP1gHYTBDz8MAwGA1UdEwEB/wQCMAAwDgYDVR0P\nAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQA98OXTem8mC1G2SFJIi5IuiX6c\ntlhU+FeRGXZip2MeZg3YLkQoXujQMn4a9K8yviy+16Ww5+Nkhlg/Ff5xiaZcbYiH\n3nE/kMgK8Ag0wgloNe1MJvQeR14eb2hu/gum7QjPnBwuCaAb4eV6sxeGaUhOmz8X\nHSqL0YQxI5A8NmFymcs/imEwJsheMkeVN8f4klG5cHOQF5UDlwqxB/yPogfD8z1O\nYyviJU/Dmzan6IxXBHBsu70rNtMWT7JN0gelM/I9x4XHAfZyweNWrcFhyw7fZQku\nieKVl/wgJNJORNJLo+ZueEjtakC/TcKCXk8Ec6NNR77Eu0IY2g0n2Y6cajkX\n-----END CERTIFICATE-----\n", + "ownedBy": "644160558196", + "creationDate": { + "__class__": "datetime", + "year": 2024, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + }, + "customerVersion": 1, + "transferData": {}, + "generationId": "92e23901-f03b-488f-a65c-bed291c17cd0", + "validity": { + "notBefore": { + "__class__": "datetime", + "year": 2024, + "month": 9, + "day": 9, + "hour": 16, + "minute": 12, + "second": 8, + "microsecond": 0 + }, + "notAfter": { + "__class__": "datetime", + "year": 2049, + "month": 12, + "day": 31, + "hour": 18, + "minute": 59, + "second": 59, + "microsecond": 0 + } + }, + "certificateMode": "DEFAULT" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_certificate_age/iot.ListCertificates_1.json b/tests/data/placebo/test_iot_certificate_age/iot.ListCertificates_1.json new file mode 100644 index 00000000000..623335a8245 --- /dev/null +++ b/tests/data/placebo/test_iot_certificate_age/iot.ListCertificates_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "certificates": [ + { + "certificateArn": "arn:aws:iot:us-east-1:644160558196:cert/6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "certificateId": "6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "status": "ACTIVE", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_certificate_set_inactive/iot.DescribeCertificate_1.json b/tests/data/placebo/test_iot_certificate_set_inactive/iot.DescribeCertificate_1.json new file mode 100644 index 00000000000..aa01fcd38f2 --- /dev/null +++ b/tests/data/placebo/test_iot_certificate_set_inactive/iot.DescribeCertificate_1.json @@ -0,0 +1,59 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "certificateDescription": { + "certificateArn": "arn:aws:iot:us-east-1:644160558196:cert/6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "certificateId": "6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "status": "ACTIVE", + "certificatePem": "-----BEGIN CERTIFICATE-----\nMIIDWTCCAkGgAwIBAgIUWFTN5o/slZ/2twBgyviexrtCgD0wDQYJKoZIhvcNAQEL\nBQAwTTFLMEkGA1UECwxCQW1hem9uIFdlYiBTZXJ2aWNlcyBPPUFtYXpvbi5jb20g\nSW5jLiBMPVNlYXR0bGUgU1Q9V2FzaGluZ3RvbiBDPVVTMB4XDTI2MDkwOTIwMTIw\nOFoXDTQ5MTIzMTIzNTk1OVowHjEcMBoGA1UEAwwTQVdTIElvVCBDZXJ0aWZpY2F0\nZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALC7RWgH/gYXudBiZhha\nOUmQiLbsi00NrXBCSNgi0j1Knb3BNr2ERifuph+FWmHE0GTFlwnhkJdxW+GDUxf4\n3ZpNPZvFD9LzXZiBz2b2z6NXhtqhShzP4TEcG2yOZc2+zmGTDs6psPBW2erkFQC3\nFcMEn3mYE8/gzsrvgR7kW5xYZstB9ahCykrCSkOH1ZQ2TtGI84Kuj81xIZ7OoSxS\nxT3uIZVtVeyhf2hjEsMof9syLjlUd6C0guafepXXS1WncdGfoeD1cc8ZbUi/MiVJ\nqJDNQ8jBMeDvVP3cb8Gyx3ye016xKK9T6DTj9/f36RGKKFo98nmQdDKoQXbtHV4D\nhbECAwEAAaNgMF4wHwYDVR0jBBgwFoAUaJ/QZNsnbsmF0cLGMsn5JgI9G9cwHQYD\nVR0OBBYEFGt3RQsx2JO1fLnKqLP1gHYTBDz8MAwGA1UdEwEB/wQCMAAwDgYDVR0P\nAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4IBAQA98OXTem8mC1G2SFJIi5IuiX6c\ntlhU+FeRGXZip2MeZg3YLkQoXujQMn4a9K8yviy+16Ww5+Nkhlg/Ff5xiaZcbYiH\n3nE/kMgK8Ag0wgloNe1MJvQeR14eb2hu/gum7QjPnBwuCaAb4eV6sxeGaUhOmz8X\nHSqL0YQxI5A8NmFymcs/imEwJsheMkeVN8f4klG5cHOQF5UDlwqxB/yPogfD8z1O\nYyviJU/Dmzan6IxXBHBsu70rNtMWT7JN0gelM/I9x4XHAfZyweNWrcFhyw7fZQku\nieKVl/wgJNJORNJLo+ZueEjtakC/TcKCXk8Ec6NNR77Eu0IY2g0n2Y6cajkX\n-----END CERTIFICATE-----\n", + "ownedBy": "644160558196", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + }, + "customerVersion": 1, + "transferData": {}, + "generationId": "92e23901-f03b-488f-a65c-bed291c17cd0", + "validity": { + "notBefore": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 12, + "second": 8, + "microsecond": 0 + }, + "notAfter": { + "__class__": "datetime", + "year": 2049, + "month": 12, + "day": 31, + "hour": 18, + "minute": 59, + "second": 59, + "microsecond": 0 + } + }, + "certificateMode": "DEFAULT" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_certificate_set_inactive/iot.ListCertificates_1.json b/tests/data/placebo/test_iot_certificate_set_inactive/iot.ListCertificates_1.json new file mode 100644 index 00000000000..623335a8245 --- /dev/null +++ b/tests/data/placebo/test_iot_certificate_set_inactive/iot.ListCertificates_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "certificates": [ + { + "certificateArn": "arn:aws:iot:us-east-1:644160558196:cert/6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "certificateId": "6969630f630be222c36e9860129f7dd6a9ad132264e8a28b695043c2e68ed7a5", + "status": "ACTIVE", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 16, + "minute": 14, + "second": 8, + "microsecond": 401000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_certificate_set_inactive/iot.UpdateCertificate_1.json b/tests/data/placebo/test_iot_certificate_set_inactive/iot.UpdateCertificate_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_certificate_set_inactive/iot.UpdateCertificate_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_configured_disabled/iam.ListAccountAliases_1.json b/tests/data/placebo/test_iot_logging_configured_disabled/iam.ListAccountAliases_1.json new file mode 100644 index 00000000000..47853b72334 --- /dev/null +++ b/tests/data/placebo/test_iot_logging_configured_disabled/iam.ListAccountAliases_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 200, + "data": { + "AccountAliases": [ + "cloud-compass-xacc" + ], + "IsTruncated": false, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_configured_disabled/iot.GetV2LoggingOptions_1.json b/tests/data/placebo/test_iot_logging_configured_disabled/iot.GetV2LoggingOptions_1.json new file mode 100644 index 00000000000..758f3f2bd59 --- /dev/null +++ b/tests/data/placebo/test_iot_logging_configured_disabled/iot.GetV2LoggingOptions_1.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-iot-logging-role", + "defaultLogLevel": "DISABLED", + "disableAllLogs": false, + "eventConfigurations": [ + { + "eventType": "Connection.AuthNError", + "logLevel": "DISABLED" + }, + { + "eventType": "Ping", + "logLevel": "DISABLED" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_enabled/iam.ListAccountAliases_1.json b/tests/data/placebo/test_iot_logging_enabled/iam.ListAccountAliases_1.json new file mode 100644 index 00000000000..47853b72334 --- /dev/null +++ b/tests/data/placebo/test_iot_logging_enabled/iam.ListAccountAliases_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 200, + "data": { + "AccountAliases": [ + "cloud-compass-xacc" + ], + "IsTruncated": false, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_enabled/iot.GetV2LoggingOptions_1.json b/tests/data/placebo/test_iot_logging_enabled/iot.GetV2LoggingOptions_1.json new file mode 100644 index 00000000000..75fec3e03ad --- /dev/null +++ b/tests/data/placebo/test_iot_logging_enabled/iot.GetV2LoggingOptions_1.json @@ -0,0 +1,19 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "roleArn": "arn:aws:iam::644160558196:role/c7n-test-iot-logging-role", + "defaultLogLevel": "ERROR", + "disableAllLogs": false, + "eventConfigurations": [ + { + "eventType": "Connection.AuthNError", + "logLevel": "DISABLED" + }, + { + "eventType": "Ping", + "logLevel": "DISABLED" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_not_configured/iam.ListAccountAliases_1.json b/tests/data/placebo/test_iot_logging_not_configured/iam.ListAccountAliases_1.json new file mode 100644 index 00000000000..47853b72334 --- /dev/null +++ b/tests/data/placebo/test_iot_logging_not_configured/iam.ListAccountAliases_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 200, + "data": { + "AccountAliases": [ + "cloud-compass-xacc" + ], + "IsTruncated": false, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_logging_not_configured/iot.GetV2LoggingOptions_1.json b/tests/data/placebo/test_iot_logging_not_configured/iot.GetV2LoggingOptions_1.json new file mode 100644 index 00000000000..af01a451c24 --- /dev/null +++ b/tests/data/placebo/test_iot_logging_not_configured/iot.GetV2LoggingOptions_1.json @@ -0,0 +1,11 @@ +{ + "status_code": 404, + "data": { + "Error": { + "Message": "SetV2LoggingOptions was not previously called. No logging options have been set.", + "Code": "NotConfiguredException" + }, + "ResponseMetadata": {}, + "message": "SetV2LoggingOptions was not previously called. No logging options have been set." + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_1.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_1.json new file mode 100644 index 00000000000..7d65b9df612 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_1.json @@ -0,0 +1,52 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdateInfo": { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 24, + "microsecond": 696000 + }, + "targets": [ + "arn:aws:iot:us-east-1:644160558196:thing/MyLightBulb" + ], + "protocols": [ + "MQTT" + ], + "awsJobExecutionsRolloutConfig": {}, + "awsJobPresignedUrlConfig": {}, + "targetSelection": "SNAPSHOT", + "otaUpdateFiles": [ + { + "fileName": "firmware.bin", + "fileVersion": "1", + "fileLocation": { + "s3Location": { + "bucket": "c7n-test-iot-ota-644160558196", + "key": "firmware.bin" + } + } + } + ], + "otaUpdateStatus": "CREATE_COMPLETE" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_2.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_2.json new file mode 100644 index 00000000000..7d65b9df612 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.GetOTAUpdate_2.json @@ -0,0 +1,52 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdateInfo": { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 24, + "microsecond": 696000 + }, + "targets": [ + "arn:aws:iot:us-east-1:644160558196:thing/MyLightBulb" + ], + "protocols": [ + "MQTT" + ], + "awsJobExecutionsRolloutConfig": {}, + "awsJobPresignedUrlConfig": {}, + "targetSelection": "SNAPSHOT", + "otaUpdateFiles": [ + { + "fileName": "firmware.bin", + "fileVersion": "1", + "fileLocation": { + "s3Location": { + "bucket": "c7n-test-iot-ota-644160558196", + "key": "firmware.bin" + } + } + } + ], + "otaUpdateStatus": "CREATE_COMPLETE" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_1.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_1.json new file mode 100644 index 00000000000..d236d29f28c --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_1.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdates": [ + { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_2.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_2.json new file mode 100644 index 00000000000..d236d29f28c --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListOTAUpdates_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdates": [ + { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_2.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_2.json new file mode 100644 index 00000000000..76ffe005960 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "Key": "lob", + "Value": "overhead" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_3.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_3.json new file mode 100644 index 00000000000..76ffe005960 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "Key": "lob", + "Value": "overhead" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_4.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_4.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.ListTagsForResource_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.TagResource_1.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.TagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.TagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_tag_untag/iot.UntagResource_1.json b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.UntagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_tag_untag/iot.UntagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_unsigned/iot.GetOTAUpdate_1.json b/tests/data/placebo/test_iot_ota_update_unsigned/iot.GetOTAUpdate_1.json new file mode 100644 index 00000000000..c0bf60d8324 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_unsigned/iot.GetOTAUpdate_1.json @@ -0,0 +1,56 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdateInfo": { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 24, + "microsecond": 696000 + }, + "targets": [ + "arn:aws:iot:us-east-1:644160558196:thing/MyLightBulb" + ], + "protocols": [ + "MQTT" + ], + "awsJobExecutionsRolloutConfig": {}, + "awsJobPresignedUrlConfig": {}, + "targetSelection": "SNAPSHOT", + "otaUpdateFiles": [ + { + "fileName": "firmware.bin", + "fileVersion": "1", + "fileLocation": { + "s3Location": { + "bucket": "c7n-test-iot-ota-644160558196", + "key": "firmware.bin" + } + } + } + ], + "otaUpdateStatus": "CREATE_FAILED", + "errorInfo": { + "code": "AWSIotException", + "message": "User: arn:aws:sts::644160558196:assumed-role/c7n-test-iot-ota-role/943de91e2d4f48b89486889171dababe is not authorized to perform: iot:CreateStream on resource: arn:aws:iot:us-east-1:644160558196:stream/AFR_OTA-96b5f25d-3ac0-4764-9799-f5e8657f6334 because no identity-based policy allows the iot:CreateStream action (Service: AWSIot; Status Code: 403; Error Code: AccessDeniedException; Request ID: 711ff242-c010-4193-82ec-a39cb0d98282; Proxy: null)" + } + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListOTAUpdates_1.json b/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListOTAUpdates_1.json new file mode 100644 index 00000000000..d236d29f28c --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListOTAUpdates_1.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "otaUpdates": [ + { + "otaUpdateId": "c7n-test-ota-unsigned", + "otaUpdateArn": "arn:aws:iot:us-east-1:644160558196:otaupdate/c7n-test-ota-unsigned", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 10, + "minute": 2, + "second": 23, + "microsecond": 533000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_ota_update_unsigned/iot.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.DeletePolicy_1.json b/tests/data/placebo/test_iot_policy_delete/iot.DeletePolicy_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.DeletePolicy_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.GetPolicy_1.json b/tests/data/placebo/test_iot_policy_delete/iot.GetPolicy_1.json new file mode 100644 index 00000000000..969575af715 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.GetPolicy_1.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"iot:*\",\"Resource\":\"*\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "generationId": "0ae7f702ec9065f88943b57fb0ddc034bbde46e538cfd0b4985ddcfdb9b2ef3c" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_1.json b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_1.json new file mode 100644 index 00000000000..84ae400e74b --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [ + { + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_2.json b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_2.json new file mode 100644 index 00000000000..0acf2160a0a --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicies_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.ListPolicyVersions_1.json b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicyVersions_1.json new file mode 100644 index 00000000000..ae3e9af66a1 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.ListPolicyVersions_1.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyVersions": [ + { + "versionId": "1", + "isDefaultVersion": true, + "createDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_policy_delete/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_delete/iot.ListTargetsForPolicy_1.json b/tests/data/placebo/test_iot_policy_delete/iot.ListTargetsForPolicy_1.json new file mode 100644 index 00000000000..0ef93e56dfe --- /dev/null +++ b/tests/data/placebo/test_iot_policy_delete/iot.ListTargetsForPolicy_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "targets": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_1.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_1.json new file mode 100644 index 00000000000..969575af715 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_1.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"iot:*\",\"Resource\":\"*\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "generationId": "0ae7f702ec9065f88943b57fb0ddc034bbde46e538cfd0b4985ddcfdb9b2ef3c" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_2.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_2.json new file mode 100644 index 00000000000..969575af715 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.GetPolicy_2.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"iot:*\",\"Resource\":\"*\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "generationId": "0ae7f702ec9065f88943b57fb0ddc034bbde46e538cfd0b4985ddcfdb9b2ef3c" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_1.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_1.json new file mode 100644 index 00000000000..84ae400e74b --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [ + { + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_2.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_2.json new file mode 100644 index 00000000000..84ae400e74b --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListPolicies_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [ + { + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_2.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_2.json new file mode 100644 index 00000000000..76ffe005960 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_2.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "Key": "lob", + "Value": "overhead" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_3.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_3.json new file mode 100644 index 00000000000..76ffe005960 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_3.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "Key": "lob", + "Value": "overhead" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_4.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_4.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.ListTagsForResource_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.TagResource_1.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.TagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.TagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_tag_untag/iot.UntagResource_1.json b/tests/data/placebo/test_iot_policy_tag_untag/iot.UntagResource_1.json new file mode 100644 index 00000000000..5b2170a073c --- /dev/null +++ b/tests/data/placebo/test_iot_policy_tag_untag/iot.UntagResource_1.json @@ -0,0 +1,6 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_unattached/iot.GetPolicy_1.json b/tests/data/placebo/test_iot_policy_unattached/iot.GetPolicy_1.json new file mode 100644 index 00000000000..969575af715 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_unattached/iot.GetPolicy_1.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"iot:*\",\"Resource\":\"*\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "generationId": "0ae7f702ec9065f88943b57fb0ddc034bbde46e538cfd0b4985ddcfdb9b2ef3c" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_unattached/iot.ListPolicies_1.json b/tests/data/placebo/test_iot_policy_unattached/iot.ListPolicies_1.json new file mode 100644 index 00000000000..84ae400e74b --- /dev/null +++ b/tests/data/placebo/test_iot_policy_unattached/iot.ListPolicies_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [ + { + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_unattached/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_policy_unattached/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..76ffe005960 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_unattached/iot.ListTagsForResource_1.json @@ -0,0 +1,12 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [ + { + "Key": "lob", + "Value": "overhead" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_unattached/iot.ListTargetsForPolicy_1.json b/tests/data/placebo/test_iot_policy_unattached/iot.ListTargetsForPolicy_1.json new file mode 100644 index 00000000000..0ef93e56dfe --- /dev/null +++ b/tests/data/placebo/test_iot_policy_unattached/iot.ListTargetsForPolicy_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "targets": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_1.json b/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_1.json new file mode 100644 index 00000000000..969575af715 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_1.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"iot:*\",\"Resource\":\"*\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 26, + "microsecond": 656000 + }, + "generationId": "0ae7f702ec9065f88943b57fb0ddc034bbde46e538cfd0b4985ddcfdb9b2ef3c" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_2.json b/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_2.json new file mode 100644 index 00000000000..48725a0b5ad --- /dev/null +++ b/tests/data/placebo/test_iot_policy_wildcard/iot.GetPolicy_2.json @@ -0,0 +1,31 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policyName": "c7n-test-iot-scoped", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-scoped", + "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"iot:Connect\"],\"Resource\":\"arn:aws:iot:us-east-1:644160558196:client/${iot:ClientId}\"},{\"Effect\":\"Allow\",\"Action\":[\"iot:Publish\"],\"Resource\":\"arn:aws:iot:us-east-1:644160558196:topic/devices/telemetry\"}]}", + "defaultVersionId": "1", + "creationDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 48, + "microsecond": 978000 + }, + "lastModifiedDate": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 15, + "minute": 16, + "second": 48, + "microsecond": 978000 + }, + "generationId": "0a530c0b140d2a5888d551cf1a1aa98ea6d2c38fb3ca98b7163044fcc554fe5a" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_wildcard/iot.ListPolicies_1.json b/tests/data/placebo/test_iot_policy_wildcard/iot.ListPolicies_1.json new file mode 100644 index 00000000000..41752125097 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_wildcard/iot.ListPolicies_1.json @@ -0,0 +1,16 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "policies": [ + { + "policyName": "c7n-test-iot-wildcard", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-wildcard" + }, + { + "policyName": "c7n-test-iot-scoped", + "policyArn": "arn:aws:iot:us-east-1:644160558196:policy/c7n-test-iot-scoped" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_1.json b/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_1.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_2.json b/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_2.json new file mode 100644 index 00000000000..ad561a828e5 --- /dev/null +++ b/tests/data/placebo/test_iot_policy_wildcard/iot.ListTagsForResource_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "tags": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_iot_thing_query/iot.ListThings_1.json b/tests/data/placebo/test_iot_thing_query/iot.ListThings_1.json new file mode 100644 index 00000000000..e2fcb0c3fcf --- /dev/null +++ b/tests/data/placebo/test_iot_thing_query/iot.ListThings_1.json @@ -0,0 +1,17 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "things": [ + { + "thingName": "MyLightBulb", + "thingArn": "arn:aws:iot:us-east-1:644160558196:thing/MyLightBulb", + "attributes": { + "model": "123", + "wattage": "75" + }, + "version": 1 + } + ] + } +} \ No newline at end of file diff --git a/tests/test_iot.py b/tests/test_iot.py new file mode 100644 index 00000000000..5c56ca6aef0 --- /dev/null +++ b/tests/test_iot.py @@ -0,0 +1,289 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +from .common import BaseTest + + +class IoTThingTest(BaseTest): + + def test_iot_thing_query(self): + factory = self.replay_flight_data("test_iot_thing_query") + p = self.load_policy( + {"name": "iot-thing", "resource": "aws.iot"}, + session_factory=factory, + ) + resources = p.run() + self.assertTrue(len(resources) == 1) + + +class IoTPolicyTest(BaseTest): + + def test_iot_policy_wildcard(self): + factory = self.replay_flight_data("test_iot_policy_wildcard") + p = self.load_policy( + { + "name": "iot-policy-wildcard", + "resource": "aws.iot-policy", + "filters": [ + { + "type": "has-statement", + "statements": [ + { + "Effect": "Allow", + "Action": "iot:*", + "PartialMatch": "Action", + } + ], + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_iot_policy_unattached(self): + factory = self.replay_flight_data("test_iot_policy_unattached") + p = self.load_policy( + { + "name": "iot-policy-orphaned", + "resource": "aws.iot-policy", + "filters": [{"type": "attached", "state": False}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["c7n:Targets"], []) + + def test_iot_policy_delete(self): + factory = self.replay_flight_data("test_iot_policy_delete") + p = self.load_policy( + { + "name": "iot-policy-delete", + "resource": "aws.iot-policy", + "filters": [{"type": "attached", "state": False}], + "actions": [{"type": "delete"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + client = factory().client("iot") + self.assertNotIn( + resources[0]["policyName"], + [p["policyName"] for p in client.list_policies()["policies"]]) + + def test_iot_policy_tag_untag(self): + factory = self.replay_flight_data("test_iot_policy_tag_untag") + p = self.load_policy( + { + "name": "iot-policy-tag", + "resource": "aws.iot-policy", + "filters": [{"tag:lob": "absent"}], + "actions": [{"type": "tag", "tags": {"lob": "overhead"}}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + client = factory().client("iot") + tags = client.list_tags_for_resource( + resourceArn=resources[0]["policyArn"])["tags"] + self.assertEqual( + {t["Key"]: t["Value"] for t in tags}, {"lob": "overhead"}) + + p = self.load_policy( + { + "name": "iot-policy-untag", + "resource": "aws.iot-policy", + "filters": [{"tag:lob": "overhead"}], + "actions": [{"type": "remove-tag", "tags": ["lob"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + tags = client.list_tags_for_resource( + resourceArn=resources[0]["policyArn"])["tags"] + self.assertEqual(tags, []) + + +class IoTCertificateTest(BaseTest): + + def test_iot_certificate_age(self): + factory = self.replay_flight_data("test_iot_certificate_age") + p = self.load_policy( + { + "name": "iot-cert-age", + "resource": "aws.iot-certificate", + "filters": [ + {"type": "value", "key": "status", "value": "ACTIVE"}, + { + "type": "value", + "key": "creationDate", + "value_type": "age", + "op": "greater-than", + "value": 365, + }, + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_iot_certificate_set_inactive(self): + factory = self.replay_flight_data("test_iot_certificate_set_inactive") + p = self.load_policy( + { + "name": "iot-cert-deactivate", + "resource": "aws.iot-certificate", + "filters": [{"type": "value", "key": "status", "value": "ACTIVE"}], + "actions": [{"type": "set-inactive"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + +class IoTOTAUpdateTest(BaseTest): + + def test_iot_ota_update_unsigned(self): + factory = self.replay_flight_data("test_iot_ota_update_unsigned") + p = self.load_policy( + { + "name": "iot-ota-unsigned", + "resource": "aws.iot-ota-update", + "filters": [{"type": "unsigned"}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + def test_iot_ota_update_tag_untag(self): + factory = self.replay_flight_data("test_iot_ota_update_tag_untag") + p = self.load_policy( + { + "name": "iot-ota-tag", + "resource": "aws.iot-ota-update", + "filters": [{"tag:lob": "absent"}], + "actions": [{"type": "tag", "tags": {"lob": "overhead"}}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + client = factory().client("iot") + tags = client.list_tags_for_resource( + resourceArn=resources[0]["otaUpdateArn"])["tags"] + self.assertEqual( + {t["Key"]: t["Value"] for t in tags}, {"lob": "overhead"}) + + p = self.load_policy( + { + "name": "iot-ota-untag", + "resource": "aws.iot-ota-update", + "filters": [{"tag:lob": "overhead"}], + "actions": [{"type": "remove-tag", "tags": ["lob"]}], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + + tags = client.list_tags_for_resource( + resourceArn=resources[0]["otaUpdateArn"])["tags"] + self.assertEqual(tags, []) + + +class IoTLoggingTest(BaseTest): + + def test_iot_logging_not_configured(self): + factory = self.replay_flight_data("test_iot_logging_not_configured") + p = self.load_policy( + { + "name": "iot-logging-not-configured", + "resource": "account", + "filters": [ + { + "type": "iot-logging", + "key": "loggingConfigured", + "value": False, + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertEqual( + resources[0]["c7n:IoTLogging"], {"loggingConfigured": False}) + + def test_iot_logging_configured_disabled(self): + factory = self.replay_flight_data("test_iot_logging_configured_disabled") + p = self.load_policy( + { + "name": "iot-logging-configured-disabled", + "resource": "account", + "filters": [ + { + "or": [ + { + "type": "iot-logging", + "key": "disableAllLogs", + "value": True, + }, + { + "type": "iot-logging", + "key": "defaultLogLevel", + "value": "DISABLED", + }, + ] + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + options = resources[0]["c7n:IoTLogging"] + self.assertTrue(options["loggingConfigured"]) + self.assertTrue( + options.get("disableAllLogs") + or options.get("defaultLogLevel") == "DISABLED") + + def test_iot_logging_configured(self): + factory = self.replay_flight_data("test_iot_logging_enabled") + p = self.load_policy( + { + "name": "iot-logging-configured", + "resource": "account", + "filters": [ + { + "type": "iot-logging", + "key": "loggingConfigured", + "value": True, + }, + { + "type": "iot-logging", + "key": "defaultLogLevel", + "op": "ne", + "value": "DISABLED", + }, + { + "type": "iot-logging", + "key": "disableAllLogs", + "value": False, + } + ], + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 1) + self.assertTrue(resources[0]["c7n:IoTLogging"]["loggingConfigured"]) From cc0c61c515d21e06421b3c9a2a58f0ef1ff99915 Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:59:26 -0500 Subject: [PATCH 15/65] testing - add diff coverage recipes to Makefile (#10630) --- Makefile | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Makefile b/Makefile index 3751c51a9ee..053bf48f61c 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,49 @@ test-coverage: --cov tools/c7n_oci/c7n_oci \ tests tools $(ARGS) +diff-coverage: + @echo "Running tests with coverage for changed files..." + @CHANGED_TEST_FILES=$$(git diff --name-only origin/main | grep -E "test_.*\.py$$" || true); \ + if [ -z "$$CHANGED_TEST_FILES" ]; then \ + echo "Error: No test files changed compared to origin/main"; \ + echo "Use 'make diff-coverage-full' to run all tests for a provider, or specify TEST_PATH="; \ + exit 1; \ + fi; \ + echo "Running changed test files:"; \ + echo "$$CHANGED_TEST_FILES" | sed 's/^/ - /'; \ + uv run pytest $$CHANGED_TEST_FILES -n auto \ + --cov-config .coveragerc \ + --cov-report xml \ + --cov . + @echo "" + @echo "Generating diff coverage report..." + @uv tool run diff-cover coverage.xml \ + --compare-branch=origin/main \ + --src-roots . \ + --exclude "tests/**" \ + --exclude "tools/**/tests/**" \ + --exclude "**/test_*.py" \ + --html-report htmlcov/diff-coverage.html + @echo "" + @echo "HTML report generated: htmlcov/diff-coverage.html" + @echo "Run 'make view-diff-coverage-linux' to open the report in your browser" + +view-coverage-linux: + @if [ ! -f htmlcov/index.html ]; then \ + echo "Error: htmlcov/index.html not found"; \ + echo "Run 'make test-coverage' first to generate the full coverage report"; \ + exit 1; \ + fi + @xdg-open htmlcov/index.html 2>/dev/null || open htmlcov/index.html 2>/dev/null || echo "Please open htmlcov/index.html manually" + +view-diff-coverage-linux: + @if [ ! -f htmlcov/diff-coverage.html ]; then \ + echo "Error: htmlcov/diff-coverage.html not found"; \ + echo "Run 'make diff-coverage' first to generate the diff coverage report"; \ + exit 1; \ + fi + @xdg-open htmlcov/diff-coverage.html 2>/dev/null || open htmlcov/diff-coverage.html 2>/dev/null || echo "Please open htmlcov/diff-coverage.html manually" + test-functional: # note this will provision real resources in a cloud environment C7N_FUNCTIONAL=yes AWS_DEFAULT_REGION=us-east-2 pytest tests -m functional $(ARGS) From 26fdff1d5729ee448bb5ec2e46b5017f29d97792 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 29 Sep 2026 11:17:47 -0600 Subject: [PATCH 16/65] gcp - add workspace-user resource for Workspace MFA reporting (#10950) --- .../gcp/examples/workspace-user-mfa.rst | 157 ++++++++++++ pyproject.toml | 4 + tools/c7n_gcp/c7n_gcp/client.py | 53 +++- tools/c7n_gcp/c7n_gcp/query.py | 9 +- .../c7n_gcp/c7n_gcp/resources/resource_map.py | 1 + tools/c7n_gcp/c7n_gcp/resources/workspace.py | 103 ++++++++ ...leapis.com-admin-directory-v1-users_1.json | 240 ++++++++++++++++++ ...s.com-v1beta1-inboundSsoAssignments_1.json | 18 ++ ...s.com-v1beta1-inboundSsoAssignments_1.json | 30 +++ .../mock-saml-metadata.xml | 30 +++ .../terraform/workspace_user_query/scrub.py | 184 ++++++++++++++ .../workspace_user_query/workspace-setup.md | 217 ++++++++++++++++ .../workspace_user_query/workspace_sso.py | 115 +++++++++ tools/c7n_gcp/tests/test_client.py | 128 ++++++++++ tools/c7n_gcp/tests/test_query.py | 1 + tools/c7n_gcp/tests/test_resource.py | 1 + tools/c7n_gcp/tests/test_workspace.py | 191 ++++++++++++++ 17 files changed, 1477 insertions(+), 5 deletions(-) create mode 100644 docs/source/gcp/examples/workspace-user-mfa.rst create mode 100644 tools/c7n_gcp/c7n_gcp/resources/workspace.py create mode 100644 tools/c7n_gcp/tests/data/flights/workspace-user-query/get-admin.googleapis.com-admin-directory-v1-users_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/workspace-user-query/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/workspace-user-sso-assignment/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json create mode 100644 tools/c7n_gcp/tests/terraform/workspace_user_query/mock-saml-metadata.xml create mode 100644 tools/c7n_gcp/tests/terraform/workspace_user_query/scrub.py create mode 100644 tools/c7n_gcp/tests/terraform/workspace_user_query/workspace-setup.md create mode 100644 tools/c7n_gcp/tests/terraform/workspace_user_query/workspace_sso.py create mode 100644 tools/c7n_gcp/tests/test_client.py create mode 100644 tools/c7n_gcp/tests/test_workspace.py diff --git a/docs/source/gcp/examples/workspace-user-mfa.rst b/docs/source/gcp/examples/workspace-user-mfa.rst new file mode 100644 index 00000000000..5e92bc5c7c8 --- /dev/null +++ b/docs/source/gcp/examples/workspace-user-mfa.rst @@ -0,0 +1,157 @@ +.. _gcp_workspace_users: + +Workspace Users - find users without MFA +======================================== + +Google Workspace (Cloud Identity) users are not GCP resources. They +are read through the Admin SDK Directory API, which differs from the +rest of the GCP provider in two ways: it is scoped to a Workspace +account, called a "customer", rather than a project, and it is +authorized by domain wide delegation rather than by GCP IAM:: + + +-----------------------------------------------------------------------+ + | GOOGLE WORKSPACE | + | | + | +---------------------------------------------------------------+ | + | | Workspace user account that can list user meta data | | + | +---------------------------------------------------------------+ | + | ^ | + +-------------------------------------------------------|---------------+ + | + 2. Impersonates User | + (OAuth 2.0 Access | + Token generated) | + | + +-------------------------------------------------------|---------------+ + | GOOGLE CLOUD PLATFORM (GCP) PROJECT | | + | | | + | +---------------------------------------------------+-----------+ | + | | GCP Service Account | | + | | - Client ID (OAuth 2.0 Identifier) | | + | | - Holds private key credentials | | + | | - Has been configured in the workspace via | | + | | domain-wide-delegation to be able to list user meta data | | + | | (admin.directory.user.readonly scope) | | + | +---------------------------------------------------------------+ | + | ^ | + | | 1. Authenticates & requests token | + | | | + | +---------------------------+---+ | + | | C7n policy run | | + | | - Uses Workspace API Client | | + | +-------------------------------+ | + +-----------------------------------------------------------------------+ + +The service account lives in a GCP project. The users, and the user whose +``Users > Read`` privilege is borrowed to read them, live in the Workspace. +The GCP service account is connected to the Workspace using +domain-wide-delegation configuration in the Workspace, so the service account +can live in any project. Which user it impersonates is chosen at run time by +``GOOGLE_WORKSPACE_SUBJECT``. + +Setup needed to use these resources +----------------------------------- + +You need a GCP service account that a Workspace super administrator has +authorized, and a Workspace user for it to impersonate. The impersonated +user needs only the ``Users > Read`` privilege, not super admin. + +1. Create a service account, in the GCP console under + IAM & Admin > Service Accounts. It can live in any project. + +2. Give it a key: select the account, then Keys > Add key > Create new key, + choose JSON, and keep the file it downloads. Delegation signs its own + JWT, so a key file is needed rather than the ambient credentials the + rest of the provider can use. + + Two of its fields matter below: + + ``client_id`` + The numeric id, shown in the console as the service account's "Unique + ID". This is what step 4 asks for. + + ``client_email`` + Identifies the service account, but is *not* what step 4 asks for. + +3. Enable the APIs these policies call, in the same project under + APIs & Services > Library: **Admin SDK API** and **Cloud Identity API**. + +4. Authorize the delegation. Sign in to https://admin.google.com as a super + administrator, go to Security > Access and data control > API controls > + Manage Domain-Wide Delegation, click "Add new", and enter the service + account's ``client_id`` with these scopes: + + ``https://www.googleapis.com/auth/admin.directory.user.readonly`` + Listing users. + + ``https://www.googleapis.com/auth/cloud-identity.inboundsso.readonly`` + The SSO check described under `Caveats`_. + + Both are required. Delegation authorizes exactly the scopes requested + and a broader grant does not imply a narrower one, so omitting either + fails the run when its token is requested. Changes here can take a few + minutes to take effect. + +Then, when running policies, the following environment variables must be set: + +``GOOGLE_APPLICATION_CREDENTIALS`` + Path to the key file downloaded above. + +``GOOGLE_WORKSPACE_SUBJECT`` + The Workspace user with the ``Users > Read`` privilege to + impersonate. Delegation is only attempted when this is set. + +In addition, you may need: + +``GOOGLE_WORKSPACE_CUSTOMER`` + Optional customer (Workspace account) id. Defaults to + ``my_customer``, which resolves to the customer (Workspace) the + impersonated subject belongs to, so the usual single Workspace case + needs no configuration. Set it when the subject can administer more + than one customer, or to pin the target explicitly. + +A run targets one customer, so scanning several means several runs. Because +the resource is customer scoped rather than project scoped, it should be +excluded from per project sweeps, which would otherwise report the same +users once per project. + +Finding users without MFA +------------------------- + +The Directory API reports 2 step verification, Google's term for MFA, per +user. Suspended users cannot sign in, so they are excluded to avoid noise. + +.. code-block:: yaml + + policies: + - name: gcp-workspace-users-without-mfa + description: | + Workspace users that have not enrolled in 2 step verification. + resource: gcp.workspace-user + filters: + - type: value + key: isEnrolledIn2Sv + value: false + - type: value + key: suspended + value: false + +Caveats +------- + +``isEnrolledIn2Sv`` and ``isEnforcedIn2Sv`` report whether a second factor is +present or required, not which type it is. The Directory user resource does +not expose security key information, so security key enforcement cannot be +audited through this resource. + +``isAdmin`` denotes a Workspace *super* administrator. Users holding +a narrower delegated role appear as ``isDelegatedAdmin`` instead, and neither +is the same as a GCP ``roles/resourcemanager.organizationAdmin`` binding. + +If your organization authenticates through an external identity provider, +sign in may be handled there rather than by Google, in which case +``isEnrolledIn2Sv`` describes a factor Google may never ask for. Which +provider governs a given user depends on the org unit and group an inbound +SSO assignment targets, and that isn't resolved here yet. Rather than report +two step verification as though it were protection, policies fail when the +Workspace has any inbound SSO assignment. diff --git a/pyproject.toml b/pyproject.toml index a5033d0719d..f5a38476921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,3 +135,7 @@ omit = ["c7n/resources/opsworks.py", "c7n/resources/qldb.py"] [tool.pytest_env] env_files = [".env", "test.env"] AWS_PROFILE = { unset = true } +# Set when recording Google Workspace flight data. Left set, they would +# make the recorded tests depend on the developer's environment. +GOOGLE_WORKSPACE_SUBJECT = { unset = true } +GOOGLE_WORKSPACE_CUSTOMER = { unset = true } diff --git a/tools/c7n_gcp/c7n_gcp/client.py b/tools/c7n_gcp/c7n_gcp/client.py index f84b3710451..e188f5048ae 100644 --- a/tools/c7n_gcp/c7n_gcp/client.py +++ b/tools/c7n_gcp/c7n_gcp/client.py @@ -78,6 +78,16 @@ def get_default_project(): return os.environ[k] +def get_workspace_subject() -> str | None: + """Workspace admin to impersonate via domain-wide delegation.""" + return os.environ.get('GOOGLE_WORKSPACE_SUBJECT') + + +def get_workspace_customer() -> str: + """Workspace customer id; 'my_customer' is the subject's own account.""" + return os.environ.get('GOOGLE_WORKSPACE_CUSTOMER', 'my_customer') + + class PaginationNotSupported(Exception): """Pagination not supported on this api.""" @@ -254,12 +264,18 @@ def client(self, service_name, version, component, **kw): - cache_discovery (bool): Whether to cache discovery docs - client_options (ClientOptions): Custom client options for location-specific endpoints + - scopes (tuple): OAuth scopes for APIs not covered by the + session wide cloud-platform scope Returns: object: An instance of repository_class. """ + credentials = self._credentials + if kw.get('scopes'): + credentials = self._scoped_credentials(kw['scopes']) + service = _create_service_api( - self._credentials, + credentials, service_name, version, kw.get('developer_key'), @@ -270,11 +286,42 @@ def client(self, service_name, version, component, **kw): return ServiceClient( gcp_service=service, component=component, - credentials=self._credentials, + credentials=credentials, rate_limiter=self._rate_limiter, - use_cached_http=self._use_cached_http, + # The thread cached http is authorized with the session + # credentials, so a client using its own credentials can't share + # it. + use_cached_http=( + self._use_cached_http and credentials is self._credentials), http=self._http) + def _scoped_credentials(self, scopes): + """Credentials for an API needing its own scopes and, for Google + Workspace, a delegated subject. + + Workspace (Admin SDK) APIs are authorized by domain wide + delegation: a service account impersonates a user (the + "subject"), who holds needed workspace admin privileges. The + service account key is required because with_subject self + signs a JWT. + + Credentials that carry fixed scopes, such as the authorized_user + credentials used by flight data replay, are returned unchanged. + """ + credentials = self._credentials + if hasattr(credentials, 'with_scopes'): + credentials = credentials.with_scopes(list(scopes)) + + subject = get_workspace_subject() + if not subject: + return credentials + if not hasattr(credentials, 'with_subject'): + raise ValueError( + "GOOGLE_WORKSPACE_SUBJECT requires service account " + "credentials supporting domain wide delegation, got %s" % ( + type(credentials).__name__,)) + return credentials.with_subject(subject) + # pylint: disable=too-many-instance-attributes, too-many-arguments class ServiceClient: diff --git a/tools/c7n_gcp/c7n_gcp/query.py b/tools/c7n_gcp/c7n_gcp/query.py index 0162a44ae24..e4a96626abd 100644 --- a/tools/c7n_gcp/c7n_gcp/query.py +++ b/tools/c7n_gcp/c7n_gcp/query.py @@ -30,7 +30,7 @@ def filter(self, resource_manager, **params): m = resource_manager.resource_type session = local_session(self.session_factory) client = session.client( - m.service, m.version, m.component) + m.service, m.version, m.component, scopes=m.scopes) # depends on resource scope if m.scope in ('project', 'zone'): @@ -171,7 +171,8 @@ def get_client(self): return local_session(self.session_factory).client( self.resource_type.service, self.resource_type.version, - self.resource_type.component) + self.resource_type.component, + scopes=self.resource_type.scopes) def get_model(self): return self.resource_type @@ -395,6 +396,10 @@ class TypeInfo(metaclass=TypeMeta): service = None version = None component = None + # OAuth scopes for APIs not covered by the session's cloud-platform + # scope (the Google Workspace Admin SDK). Requesting them also engages + # domain wide delegation when GOOGLE_WORKSPACE_SUBJECT is set. + scopes = None # resource enumeration parameters diff --git a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py index 37a31589ed5..3a2e7000d92 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/resource_map.py +++ b/tools/c7n_gcp/c7n_gcp/resources/resource_map.py @@ -136,6 +136,7 @@ "gcp.vertex-ai-metadata-store-artifact": ( "c7n_gcp.resources.vertexai.VertexAIMetadataStoreArtifact"), "gcp.vpc": "c7n_gcp.resources.network.Network", + "gcp.workspace-user": "c7n_gcp.resources.workspace.WorkspaceUser", "gcp.zone": "c7n_gcp.resources.compute.Zone", } # SPDX-License-Identifier: Apache-2.0 diff --git a/tools/c7n_gcp/c7n_gcp/resources/workspace.py b/tools/c7n_gcp/c7n_gcp/resources/workspace.py new file mode 100644 index 00000000000..bc18881fdc6 --- /dev/null +++ b/tools/c7n_gcp/c7n_gcp/resources/workspace.py @@ -0,0 +1,103 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +# The relationship between GCP and Google Workspace is genuinely confusing. +# See: +# +# docs/source/gcp/examples/workspace-user-mfa.rst + +from c7n.exceptions import PolicyExecutionError +from c7n.utils import local_session + +from c7n_gcp.client import get_workspace_customer +from c7n_gcp.provider import resources +from c7n_gcp.query import QueryResourceManager, TypeInfo + +SSO_ASSIGNMENT_SCOPES = ( + 'https://www.googleapis.com/auth/cloud-identity.inboundsso.readonly',) + + +@resources.register('workspace-user') +class WorkspaceUser(QueryResourceManager): + """Google Workspace user. + + Users live in Cloud Identity / Google Workspace rather than in GCP, so + reading them needs its own setup: see + :doc:`/gcp/examples/workspace-user-mfa` for what to configure and why. + + https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list + + :example: + + Users that have not enrolled in 2 step verification, Google's term for + MFA. Suspended users can't sign in, so they are excluded. + + .. code-block:: yaml + + policies: + - name: gcp-workspace-users-without-mfa + resource: gcp.workspace-user + filters: + - type: value + key: isEnrolledIn2Sv + value: false + - type: value + key: suspended + value: false + """ + + def get_permissions(self): + """Nothing to grant in IAM. + + Reading workspace users is authorized by an OAuth scope and the + impersonated subject's Workspace privileges, so the derived + admin.users.list would name a permission that cannot be granted. + """ + return () + + def get_resource_query(self): + return {'customer': get_workspace_customer()} + + def resources(self, query=None): + self.check_no_sso_assignments() + return super().resources(query) + + def check_no_sso_assignments(self): + """Refuse to report on users whose sign in Google may not handle. + + An InboundSsoAssignment sends a org unit or a group to an external + identity provider, and which assignment wins for a given user isn't + resolved here yet, so isEnrolledIn2Sv and isEnforcedIn2Sv can't be + read as protection. + + https://cloud.google.com/identity/docs/reference/rest/v1beta1/inboundSsoAssignments/list + """ + client = local_session(self.session_factory).client( + 'cloudidentity', 'v1beta1', 'inboundSsoAssignments', + scopes=SSO_ASSIGNMENT_SCOPES) + # The quoting is required, though the api's own example omits it. + assignments = client.execute_query( + 'list', + {'filter': 'customer=="customers/%s"' % get_workspace_customer()}) + if assignments.get('inboundSsoAssignments'): + raise PolicyExecutionError( + "Organizations with SSO assignments aren't currently" + " supported by gcp.workspace-user.") + + class resource_type(TypeInfo): + service = 'admin' + version = 'directory_v1' + component = 'users' + enum_spec = ('list', 'users[]', None) + # Customer scoped, so no project is injected into the query. + scope = None + scopes = ( + 'https://www.googleapis.com/auth/admin.directory.user.readonly',) + id = 'id' + name = 'primaryEmail' + default_report_fields = [ + 'primaryEmail', 'isAdmin', 'isEnrolledIn2Sv', 'isEnforcedIn2Sv', + 'suspended', 'orgUnitPath', 'lastLoginTime'] + urn_component = 'workspace-user' + # Workspace users do not live under a GCP project. + urn_has_project = False diff --git a/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-admin.googleapis.com-admin-directory-v1-users_1.json b/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-admin.googleapis.com-admin-directory-v1-users_1.json new file mode 100644 index 00000000000..dfc030a17ef --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-admin.googleapis.com-admin-directory-v1-users_1.json @@ -0,0 +1,240 @@ +{ + "headers": { + "etag": "\"c7n-test-users-etag\"", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 10 Aug 2026 20:09:27 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "7987", + "-content-encoding": "gzip", + "content-location": "https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&alt=json" + }, + "body": { + "kind": "admin#directory#users", + "etag": "\"c7n-test-users-etag\"", + "users": [ + { + "kind": "admin#directory#user", + "isAdmin": true, + "isDelegatedAdmin": false, + "lastLoginTime": "2026-08-07T20:25:02.000Z", + "creationTime": "2026-07-24T16:32:45.000Z", + "agreedToTerms": true, + "suspended": false, + "archived": false, + "changePasswordAtNextLogin": false, + "ipWhitelisted": false, + "languages": [ + { + "languageCode": "en", + "preference": "preferred" + } + ], + "orgUnitPath": "/", + "isMailboxSetup": false, + "isEnrolledIn2Sv": true, + "isEnforcedIn2Sv": true, + "includeInGlobalAddressList": true, + "isGuestUser": false, + "id": "100000000000000000001", + "etag": "\"c7n-test-user-1-etag\"", + "primaryEmail": "superadmin@example.com", + "name": { + "givenName": "superadmin", + "familyName": "Test", + "fullName": "superadmin Test" + }, + "emails": [ + { + "address": "superadmin@example.com", + "primary": true + } + ], + "nonEditableAliases": [ + "superadmin@example.com.test-google-a.com" + ], + "customerId": "C03abc123" + }, + { + "kind": "admin#directory#user", + "isAdmin": false, + "isDelegatedAdmin": true, + "lastLoginTime": "2026-07-28T21:31:49.000Z", + "creationTime": "2026-07-28T21:29:42.000Z", + "agreedToTerms": true, + "suspended": true, + "suspensionReason": "WEB_LOGIN_REQUIRED", + "archived": false, + "changePasswordAtNextLogin": false, + "ipWhitelisted": false, + "languages": [ + { + "languageCode": "en", + "preference": "preferred" + } + ], + "orgUnitPath": "/", + "isMailboxSetup": false, + "isEnrolledIn2Sv": true, + "isEnforcedIn2Sv": true, + "includeInGlobalAddressList": true, + "isGuestUser": false, + "suspensionTime": "2026-07-29T02:17:43.242Z", + "id": "100000000000000000002", + "etag": "\"c7n-test-user-2-etag\"", + "primaryEmail": "test_admin@example.com", + "name": { + "givenName": "test_admin", + "familyName": "Test", + "fullName": "test_admin Test" + }, + "emails": [ + { + "address": "test_admin@example.com", + "primary": true + } + ], + "nonEditableAliases": [ + "test_admin@example.com.test-google-a.com" + ], + "customerId": "C03abc123" + }, + { + "kind": "admin#directory#user", + "isAdmin": false, + "isDelegatedAdmin": false, + "lastLoginTime": "2026-07-28T21:50:22.000Z", + "creationTime": "2026-07-28T21:47:28.000Z", + "agreedToTerms": true, + "suspended": true, + "suspensionReason": "WEB_LOGIN_REQUIRED", + "archived": false, + "changePasswordAtNextLogin": false, + "ipWhitelisted": false, + "languages": [ + { + "languageCode": "en", + "preference": "preferred" + } + ], + "orgUnitPath": "/test-no-enforcement", + "isMailboxSetup": false, + "isEnrolledIn2Sv": true, + "isEnforcedIn2Sv": false, + "includeInGlobalAddressList": true, + "isGuestUser": false, + "suspensionTime": "2026-07-29T02:17:32.604Z", + "id": "100000000000000000003", + "etag": "\"c7n-test-user-3-etag\"", + "primaryEmail": "test_needno2sv@example.com", + "name": { + "givenName": "test_needno2sv", + "familyName": "Test", + "fullName": "test_needno2sv Test" + }, + "emails": [ + { + "address": "test_needno2sv@example.com", + "primary": true + } + ], + "nonEditableAliases": [ + "test_needno2sv@example.com.test-google-a.com" + ], + "customerId": "C03abc123" + }, + { + "kind": "admin#directory#user", + "isAdmin": false, + "isDelegatedAdmin": false, + "lastLoginTime": "2026-07-30T22:05:21.000Z", + "creationTime": "2026-07-28T21:47:32.000Z", + "agreedToTerms": true, + "suspended": false, + "archived": false, + "changePasswordAtNextLogin": false, + "ipWhitelisted": false, + "languages": [ + { + "languageCode": "en", + "preference": "preferred" + } + ], + "orgUnitPath": "/test-no-enforcement", + "isMailboxSetup": false, + "isEnrolledIn2Sv": false, + "isEnforcedIn2Sv": false, + "includeInGlobalAddressList": true, + "isGuestUser": false, + "id": "100000000000000000004", + "etag": "\"c7n-test-user-4-etag\"", + "primaryEmail": "test_no2sv@example.com", + "name": { + "givenName": "test_no2sv", + "familyName": "Test", + "fullName": "test_no2sv Test" + }, + "emails": [ + { + "address": "test_no2sv@example.com", + "primary": true + } + ], + "nonEditableAliases": [ + "test_no2sv@example.com.test-google-a.com" + ], + "customerId": "C03abc123" + }, + { + "kind": "admin#directory#user", + "isAdmin": false, + "isDelegatedAdmin": false, + "lastLoginTime": "2026-07-30T21:46:37.000Z", + "creationTime": "2026-07-30T21:45:45.000Z", + "agreedToTerms": true, + "suspended": true, + "suspensionReason": "ADMIN", + "archived": false, + "changePasswordAtNextLogin": false, + "ipWhitelisted": false, + "languages": [ + { + "languageCode": "en", + "preference": "preferred" + } + ], + "orgUnitPath": "/", + "isMailboxSetup": false, + "isEnrolledIn2Sv": false, + "isEnforcedIn2Sv": true, + "includeInGlobalAddressList": true, + "isGuestUser": false, + "suspensionTime": "2026-07-30T21:48:15.065Z", + "id": "100000000000000000005", + "etag": "\"c7n-test-user-5-etag\"", + "primaryEmail": "test_no2sv_susp@example.com", + "name": { + "givenName": "test_no2sv_susp", + "familyName": "Test", + "fullName": "test_no2sv_susp Test" + }, + "emails": [ + { + "address": "test_no2sv_susp@example.com", + "primary": true + } + ], + "nonEditableAliases": [ + "test_no2sv_susp@example.com.test-google-a.com" + ], + "customerId": "C03abc123" + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json b/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json new file mode 100644 index 00000000000..0519412ae3c --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/workspace-user-query/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 10 Aug 2026 20:09:26 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://cloudidentity.googleapis.com/v1beta1/inboundSsoAssignments?filter=customer%3D%3D%22customers%2Fmy_customer%22&alt=json" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/workspace-user-sso-assignment/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json b/tools/c7n_gcp/tests/data/flights/workspace-user-sso-assignment/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json new file mode 100644 index 00000000000..a0e2c1f184e --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/workspace-user-sso-assignment/get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json @@ -0,0 +1,30 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Mon, 10 Aug 2026 20:03:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "351", + "-content-encoding": "gzip", + "content-location": "https://cloudidentity.googleapis.com/v1beta1/inboundSsoAssignments?filter=customer%3D%3D%22customers%2Fmy_customer%22&alt=json" + }, + "body": { + "inboundSsoAssignments": [ + { + "ssoMode": "SAML_SSO", + "name": "inboundSsoAssignments/c7ntestassignment", + "customer": "customers/C03abc123", + "targetOrgUnit": "orgUnits/03c7ntest1", + "samlSsoInfo": { + "inboundSamlSsoProfile": "inboundSamlSsoProfiles/00c7ntest2" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/terraform/workspace_user_query/mock-saml-metadata.xml b/tools/c7n_gcp/tests/terraform/workspace_user_query/mock-saml-metadata.xml new file mode 100644 index 00000000000..421f2f6cbc2 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/workspace_user_query/mock-saml-metadata.xml @@ -0,0 +1,30 @@ + + + + + + + MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV +SzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4 +MjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK +DAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0 +RuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd +4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V +pwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b +2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ +NfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW +5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4 +khuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX +UjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L +r/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M +m0eo2USlSRTVl7QHRTuiuSThHpLKQQ== + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/workspace_user_query/scrub.py b/tools/c7n_gcp/tests/terraform/workspace_user_query/scrub.py new file mode 100644 index 00000000000..a3c7536b4d8 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/workspace_user_query/scrub.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +"""Scrub recorded workspace responses for committing as flight data. + +See workspace-setup.md, next to this script, for when to run it. + +Every field is classified below. Anything unclassified is an error rather +than a pass through, so that a field added by google later can't carry +personal data into the repo unnoticed. That's not hypothetical: a real phone +number reached a commit because hand scrubbing only looked for the fields we +happened to think of. +""" + +import json +import pathlib +import sys + +# parents[2] is tests/, from tests/terraform/workspace_user_query/ +FLIGHTS = pathlib.Path(__file__).parents[2] / 'data' / 'flights' +FLIGHT = (FLIGHTS / 'workspace-user-query' + / 'get-admin.googleapis.com-admin-directory-v1-users_1.json') +ASSIGNMENT_FLIGHT = ( + FLIGHTS / 'workspace-user-sso-assignment' + / 'get-cloudidentity.googleapis.com-v1beta1-inboundSsoAssignments_1.json') + +DOMAIN = 'example.com' +CUSTOMER_ID = 'C03abc123' +ORG_UNIT_ID = '03c7ntest1' +SAML_PROFILE_ID = '00c7ntest2' + +# Generated ids, so rewritten. targetGroup and oidcSsoInfo aren't recorded +# by any test yet, and are here so that recording one is an error until +# somebody decides what its id should become. +ASSIGNMENT_REWRITTEN = {'name', 'customer', 'targetOrgUnit', 'targetGroup', + 'samlSsoInfo', 'oidcSsoInfo'} + +# Not identifying, and the reason the fixture exists. +ASSIGNMENT_KEPT = {'ssoMode', 'rank', 'signInBehavior'} + +# Identifying, so rewritten deterministically from the local part. +REWRITTEN = {'id', 'etag', 'primaryEmail', 'name', 'emails', + 'nonEditableAliases', 'customerId'} + +# Could carry personal data and no test needs them. +DROPPED = {'phones', 'recoveryEmail', 'recoveryPhone', 'addresses', + 'externalIds', 'relations', 'websites', 'thumbnailPhotoUrl', + 'thumbnailPhotoEtag', 'aliases', 'ims', 'organizations', + 'locations', 'keywords', 'gender', 'sshPublicKeys', 'posixAccounts', + 'customSchemas', 'notes'} + +# Not identifying, and tests assert on several of them. +KEPT = {'kind', 'isAdmin', 'isDelegatedAdmin', 'isEnrolledIn2Sv', + 'isEnforcedIn2Sv', 'suspended', 'suspensionReason', 'suspensionTime', + 'archived', 'orgUnitPath', 'agreedToTerms', 'changePasswordAtNextLogin', + 'includeInGlobalAddressList', 'ipWhitelisted', 'isMailboxSetup', + 'isGuestUser', 'creationTime', 'lastLoginTime', 'deletionTime', + 'languages', 'thumbnailPhotoUrl', 'isEnrolledInAdvancedProtection'} + + +def local_part(email: str) -> str: + """Test users keep their names; the super admin is anonymized. + + The super admin is whoever recorded this, so its local part is personal. + The test_* accounts are already test names, and the state test keys off + them. + """ + local = email.split('@')[0] + return local if local.startswith('test_') else 'superadmin' + + +def scrub_user(user: dict, index: int) -> dict: + unknown = set(user) - REWRITTEN - DROPPED - KEPT + if unknown: + sys.exit( + "unclassified field(s) %s on %s.\nAdd them to REWRITTEN, DROPPED " + "or KEPT in %s, deciding for each whether it can identify " + "anyone." % (sorted(unknown), user.get('primaryEmail'), + pathlib.Path(__file__).name)) + + local = local_part(user['primaryEmail']) + email = '%s@%s' % (local, DOMAIN) + scrubbed = {k: v for k, v in user.items() if k in KEPT} + scrubbed.update({ + 'id': '10000000000000000000%d' % index, + 'etag': '"c7n-test-user-%d-etag"' % index, + 'primaryEmail': email, + 'name': {'givenName': local, 'familyName': 'Test', + 'fullName': '%s Test' % local}, + 'emails': [{'address': email, 'primary': True}], + 'nonEditableAliases': ['%s.test-google-a.com' % email], + 'customerId': CUSTOMER_ID, + }) + return scrubbed + + +def check_scrubbable(users: list) -> None: + """Refuse input that would be mangled rather than scrubbed.""" + if any(u['primaryEmail'].endswith('@' + DOMAIN) for u in users): + sys.exit( + "%s looks scrubbed already. Re-record before scrubbing: running\n" + "this twice would anonymize the test users into one identity." + % FLIGHT.name) + + # Only the recorder's own account is anonymized. More than one + # non-test_ user means they'd all collapse to the same name. + anonymized = [u['primaryEmail'] for u in users + if not u['primaryEmail'].split('@')[0].startswith('test_')] + if len(anonymized) > 1: + sys.exit( + "expected at most one non-test_ user (the super admin), got %s.\n" + "Name test accounts test_* so they survive scrubbing distinctly." + % sorted(anonymized)) + + +def scrub_assignment(assignment: dict) -> dict: + unknown = set(assignment) - ASSIGNMENT_REWRITTEN - ASSIGNMENT_KEPT + if unknown: + sys.exit( + "unclassified field(s) %s on an sso assignment.\nAdd them to " + "ASSIGNMENT_REWRITTEN or ASSIGNMENT_KEPT in %s, deciding for " + "each whether it identifies the tenant." + % (sorted(unknown), pathlib.Path(__file__).name)) + + scrubbed = {k: v for k, v in assignment.items() if k in ASSIGNMENT_KEPT} + scrubbed.update({ + 'name': 'inboundSsoAssignments/c7ntestassignment', + 'customer': 'customers/%s' % CUSTOMER_ID, + 'targetOrgUnit': 'orgUnits/%s' % ORG_UNIT_ID, + }) + if 'samlSsoInfo' in assignment: + scrubbed['samlSsoInfo'] = { + 'inboundSamlSsoProfile': + 'inboundSamlSsoProfiles/%s' % SAML_PROFILE_ID} + return scrubbed + + +def scrub_assignments() -> None: + """The empty response recorded elsewhere has nothing to scrub.""" + doc = json.loads(ASSIGNMENT_FLIGHT.read_text()) + assignments = doc['body'].get('inboundSsoAssignments') + if not assignments: + sys.exit('%s has no assignments, so it was recorded without one.' + % ASSIGNMENT_FLIGHT) + + doc['headers']['content-location'] = ( + 'https://cloudidentity.googleapis.com/v1beta1/inboundSsoAssignments' + '?filter=customer%3D%3D%22customers%2Fmy_customer%22&alt=json') + doc['body']['inboundSsoAssignments'] = [ + scrub_assignment(a) for a in assignments] + ASSIGNMENT_FLIGHT.write_text(json.dumps(doc, indent=2) + '\n') + + print('\nscrubbed %d assignment(s) in %s' % ( + len(assignments), ASSIGNMENT_FLIGHT)) + for a in doc['body']['inboundSsoAssignments']: + print(' %-12s %s' % (a['ssoMode'], a['targetOrgUnit'])) + + +def main() -> int: + doc = json.loads(FLIGHT.read_text()) + # Before anything is written, so a re-run leaves both files alone. + check_scrubbable(doc['body']['users']) + scrub_assignments() + doc['headers']['etag'] = '"c7n-test-users-etag"' + doc['headers']['content-location'] = ( + 'https://admin.googleapis.com/admin/directory/v1/users' + '?customer=my_customer&alt=json') + doc['body']['etag'] = '"c7n-test-users-etag"' + doc['body']['users'] = [ + scrub_user(u, i) + for i, u in enumerate(doc['body']['users'], 1)] + + FLIGHT.write_text(json.dumps(doc, indent=2) + '\n') + + print('scrubbed %d users in %s\n' % (len(doc['body']['users']), FLIGHT)) + for u in doc['body']['users']: + print(' %-32s enrolled=%-5s enforced=%-5s suspended=%-5s %s' % ( + u['primaryEmail'], u['isEnrolledIn2Sv'], u['isEnforcedIn2Sv'], + u['suspended'], u['orgUnitPath'])) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace-setup.md b/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace-setup.md new file mode 100644 index 00000000000..98bd507f807 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace-setup.md @@ -0,0 +1,217 @@ +# Setting up to record against a Google Workspace. + +This is the one-time setup needed to record the flight data for +`test_workspace.py`. For how the GCP project, Workspace account, service +account and impersonated user fit together, see +`docs/source/gcp/examples/workspace-user-mfa.rst`. + +1. Obtain a domain + +2. Create a workspace against that domain. + 1. See https://docs.cloud.google.com/identity/docs/how-to/set-up-cloud-identity-admin#sign-up-free + 2. Assuming you aren't a workspace customer, you'll visit + https://workspace.google.com/gcpidentity/signup?sku=identitybasic + That will walk you through creating a workspace admin and + verifying you own the domain by helping you create a TXT record + that it will read. + + Setting up users, including the admin user, will define email + addresses. This doesn't make them deliverable. This process + won't mess with the domains MX records by default. + +3. Do the setup that running these policies needs anyway: the service + account, its key, the APIs, and the domain-wide delegation entry. See + "Setup needed to use these resources" in + `docs/source/gcp/examples/workspace-user-mfa.rst`. + + Recording needs two further scopes on that same delegation entry. No + policy uses them; they are for `workspace_sso.py`, which gives the + workspace an SSO assignment to record and then removes it: + + | scope | needed for | + |-------|------------| + | `admin.directory.orgunit.readonly` | finding the org unit to assign | + | `cloud-identity.inboundsso` | creating and deleting the assignment | + + Delegation authorizes the exact scope requested, and a broader grant does + not imply a narrower one: with `cloud-identity.inboundsso` granted, + requesting `cloud-identity.inboundsso.readonly` still fails + `unauthorized_client` until it too is listed. Scope changes take a few + minutes to take effect. + +4. Set up test users. + + This is the state the tests expect. `test_workspace_user_state` asserts + it, so if that test fails, this table is what to restore the workspace to. + + | user | admin | delegated admin | 2sv enrolled | 2sv enforced | suspended | org unit | + |-------------------|-------|-----------------|--------------|--------------|-----------|------------------------| + | *the super admin* | yes | no | yes | yes | no | `/` | + | `test_admin` | no | yes | yes | yes | yes | `/` | + | `test_needno2sv` | no | no | yes | no | yes | `/test-no-enforcement` | + | `test_no2sv` | no | no | no | no | no | `/test-no-enforcement` | + | `test_no2sv_susp` | no | no | no | yes | yes | `/` | + + The documented policy in + `docs/source/gcp/examples/workspace-user-mfa.rst` filters on + `isEnrolledIn2Sv: false` *and* `suspended: false`, so: + + - `test_no2sv` is the only user it selects, so it is the finding. + - `test_no2sv_susp` differs from it in only the suspended field, so the + `suspended: false` clause is load bearing. Without that row, a one + clause policy would select the same users and the test would prove + nothing about the second clause. + - `test_admin` is a delegated admin rather than a super admin. The two + are different, and `isAdmin` alone does not find delegated admins. + - `test_needno2sv` is enrolled but not *enforced*, so `isEnrolledIn2Sv` + and `isEnforcedIn2Sv` are seen to vary independently. + + Drop any one of the four and exactly one of those properties disappears, + so all of them are needed. + + `test_no2sv` lives in `/test-no-enforcement` deliberately. With 2sv + enforced, an unenrolled user cannot sign in at all -- and signing in is + the only way to clear google's automatic suspension. Being outside + enforcement is what makes it recoverable. + + `test_no2sv_susp` can stay in the root org unit, because it never needs + to sign in. That also makes it the only unenrolled *and* enforced user, + which is what a real violation looks like. + + Enrolling 2sv needs a phone or authenticator app per account, and can + only be done by signing in as that user. There is no API for it, which + is why this step is manual. + +5. Create a sub-organization, `test-no-enforcement`, with 2sv not enforced, + and move `test_needno2sv` and `test_no2sv` into it. + + This comes before enforcement, because `test_no2sv` is never going to + enroll, and enforcement would lock it out of the sign in that step 7 + needs. + +6. Turn on 2sv enforcement on the root organization unit, following + https://knowledge.workspace.google.com/admin/security/deploy-2-step-verification#step_5_enforce_2-step_verification_optional + + **IMPORTANT! Set up 2sv on the super admin account first!** Enforcement + locks out anyone not enrolled, and the super admin is the account the + tests impersonate. + +7. Get the suspended column to match the table. + + Suspend `test_no2sv_susp` yourself, from the admin console, following + https://knowledge.workspace.google.com/admin/users/suspend-a-user-temporarily + Don't wait for google to do it: doing it deliberately is reproducible, + and shows up as `suspensionReason: ADMIN`. + + `test_no2sv` has to be *un*suspended, and that's the awkward one. + + Google auto suspends freshly created accounts with + `suspensionReason: WEB_LOGIN_REQUIRED` when you create several in a + short period. It's a periodic sweep rather than a per account timer: our + accounts were created up to 18 minutes apart and were all suspended + within 11 seconds of each other, about 4.5 hours later. So spacing + creation out by a few minutes doesn't help much. What does help is + creating the account you need unsuspended last, on its own, and + recording promptly. + + Once it happens, **an admin cannot clear it** -- the REACTIVATE button + described at + https://knowledge.workspace.google.com/admin/users/restore-a-suspended-user + is greyed out. Only the user can, by signing in at + https://accounts.google.com and entering a code sent to a mobile phone. + Two surprises there: + + - It asks for a phone number rather than using the one on the account. + That's abuse throttling, not authentication. + - It rate limits per phone number, so the number you used to enroll 2sv + on the other accounts will likely be refused with "This phone number + has already been used too many times for verification". You may need + to borrow a phone. + + This is only possible at all because step 5 put `test_no2sv` outside 2sv + enforcement: an unenrolled user can't complete a sign in while + enforcement applies. And it matters because if `test_no2sv` stays + suspended, the documented policy selects nothing and the test proves + nothing. + + The remaining users can be suspended or not; no test depends on them. + The table records what we happened to have. + +8. Assign some admin role to test_admin. It doesn't matter which + one. Whatever is first in the list is fine. :) That makes it a + delegated admin, which is all the tests care about. + +Nothing here sets up SSO. `test_sso_assignments_are_rejected` needs the +workspace to have an inbound SSO assignment, and `workspace_sso.py` creates +one while recording and removes it afterwards, so the resting state has +none. Its identity provider is mocksaml.com, whose published metadata is +kept next to this file as `mock-saml-metadata.xml`; nothing signs in through +it, it just has to be a real enough provider for Google to accept. + +An assignment needs a SAML profile, and a profile is rejected as "not +complete" until a certificate has been added to it, so all three are created +and then deleted in reverse. + +## Recording + +Temporarily change `replay_flight_data` to `record_flight_data` in the test +you want to record, then: + +```bash +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.json +export GOOGLE_WORKSPACE_SUBJECT= +export GOOGLE_CLOUD_PROJECT=cloud-custodian +uv run pytest tools/c7n_gcp/tests/test_workspace.py -k -s -p no:env +``` + +`-p no:env` matters. `pyproject.toml` has a `[tool.pytest_env]` section that +loads `test.env` from *inside* pytest, which would otherwise override the +exports above and put the committed fake credentials back. That section also +deliberately unsets `GOOGLE_WORKSPACE_SUBJECT` and +`GOOGLE_WORKSPACE_CUSTOMER`, so that replaying recorded data doesn't depend +on whatever a developer happens to have in their environment. + +Then scrub the recording before committing it, with: + +```bash +python tools/c7n_gcp/tests/terraform/workspace_user_query/scrub.py +``` + +The recorder sanitizes GCP project names only, so everything else about real +users comes through. Don't scrub by hand: we did that once and shipped a real +phone number, because we checked for the fields we thought of rather than the +fields that were there. The script replaces the domain, the super admin's +identity, ids, etags and the customer id, and drops anything that could carry +personal data. It fails if it sees a field it doesn't recognize, so a future +API addition can't slip PII through unnoticed. It scrubs the recorded SSO +assignment the same way, rewriting the ids google generated for the +assignment, org unit and profile. + +The `test_*` local parts are kept as they are: they're already test names, and +`test_workspace_user_state` keys off them. + +Watch for stray recordings of unrelated calls as well; delete any that the +test doesn't need. Recording always leaves at least one +`...serviceAccounts-...-allowedLocations_N.json`, from signing the +delegation JWT. Replay uses fake credentials and never asks for it, and its +name carries the real project, so delete those. + +Each test's replay starts numbering responses again from `_1`, so two tests +cannot share a flight directory when they need different answers to the same +call. That's why the SSO test records into `workspace-user-sso-assignment` +while everything else uses `workspace-user-query`: one needs an assignment +listed, the others need none. + +Finally, change the test back to `replay_flight_data` and confirm it passes +with no other edits. + +## If you get 403s on everything + +During initial setup we got 403 ("Not Authorized to access this +resource/api") on every Directory API call, including reading the subject's +own record, with all of the above configured correctly. It cleared after a +while with no configuration change on our part -- though clicking around the +admin console may have had some Heisenberg effect. IDK. + +So if you hit this and the setup above checks out, wait a bit and retry +before you start rearranging things. diff --git a/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace_sso.py b/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace_sso.py new file mode 100644 index 00000000000..ed4c82d9e28 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/workspace_user_query/workspace_sso.py @@ -0,0 +1,115 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +"""Give the workspace an SSO assignment, for recording only. + +See workspace-setup.md, next to this module, for when this runs. +""" + +import os +import pathlib +import re +import textwrap + +from google.oauth2 import service_account +from googleapiclient import discovery + +from c7n_gcp.client import get_workspace_customer, get_workspace_subject + +SCOPES = ( + 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', + 'https://www.googleapis.com/auth/cloud-identity.inboundsso', + ) +ORG_UNIT_PATH = '/test-no-enforcement' +IDP_METADATA = pathlib.Path(__file__).parent / 'mock-saml-metadata.xml' + + +def add_sso_assignment(test) -> None: + """Federate ORG_UNIT_PATH to mocksaml.com, undone when the test ends. + + An assignment needs a profile, and a profile is rejected as "not + complete" until it has a credential, so all three are created. + """ + credentials = service_account.Credentials.from_service_account_file( + os.environ['GOOGLE_APPLICATION_CREDENTIALS'], + scopes=list(SCOPES), + subject=get_workspace_subject(), + ) + customer = 'customers/%s' % get_workspace_customer() + identity = discovery.build( + 'cloudidentity', 'v1beta1', credentials=credentials) + + profile = identity.inboundSamlSsoProfiles().create( + body={ + 'customer': customer, + 'displayName': 'c7n test mocksaml', + 'idpConfig': { + 'entityId': _idp_entity_id(), + 'singleSignOnServiceUri': _idp_sign_in_uri(), + }, + }, + ).execute()['response']['name'] + test.addCleanup(_delete, identity.inboundSamlSsoProfiles(), profile) + + identity.inboundSamlSsoProfiles().idpCredentials().add( + parent=profile, + body={'pemData': _idp_certificate()}, + ).execute() + + assignment = identity.inboundSsoAssignments().create( + body={ + 'customer': customer, + 'targetOrgUnit': 'orgUnits/%s' % _org_unit_id(credentials), + 'ssoMode': 'SAML_SSO', + 'samlSsoInfo': {'inboundSamlSsoProfile': profile}, + }, + ).execute()['response']['name'] + # Cleanups run last registered first, so the assignment goes before the + # profile it refers to. + test.addCleanup(_delete, identity.inboundSsoAssignments(), assignment) + + +def _delete(component, name: str) -> None: + component.delete(name=name).execute() + + +def _org_unit_id(credentials) -> str: + """Look the org unit up by path, because only its path is predictable. + + The Directory api prefixes the id with `id:`, which Cloud Identity + rejects with a 500 rather than a complaint, so drop it. + """ + directory = discovery.build( + 'admin', 'directory_v1', credentials=credentials) + units = directory.orgunits().list( + customerId=get_workspace_customer(), type='all').execute() + for unit in units['organizationUnits']: + if unit['orgUnitPath'] == ORG_UNIT_PATH: + return unit['orgUnitId'].removeprefix('id:') + raise RuntimeError( + '%s missing, see the workspace-setup.md next to %s' + % (ORG_UNIT_PATH, pathlib.Path(__file__).name)) + + +def _idp_metadata() -> str: + return IDP_METADATA.read_text() + + +def _idp_entity_id() -> str: + return re.search(r'entityID="([^"]+)"', _idp_metadata()).group(1) + + +def _idp_sign_in_uri() -> str: + """The HTTP-Redirect endpoint, the binding Google assumes.""" + return re.search( + r'<[^>]*SingleSignOnService[^>]*Binding="[^"]*HTTP-Redirect"' + r'[^>]*Location="([^"]+)"', + _idp_metadata()).group(1) + + +def _idp_certificate() -> str: + certificate = re.search( + r'<(?:\w+:)?X509Certificate>\s*([^<]+?)\s*', + _idp_metadata(), re.S).group(1) + return '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' % ( + '\n'.join(textwrap.wrap(''.join(certificate.split()), 64))) diff --git a/tools/c7n_gcp/tests/test_client.py b/tools/c7n_gcp/tests/test_client.py new file mode 100644 index 00000000000..0f3054cef22 --- /dev/null +++ b/tools/c7n_gcp/tests/test_client.py @@ -0,0 +1,128 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import pathlib +from unittest import mock + +import google.auth +import pytest + +from c7n_gcp.client import ( + Session, + get_workspace_customer, + get_workspace_subject, +) + +WORKSPACE_SCOPE = 'https://www.googleapis.com/auth/admin.directory.user.readonly' + +# The credentials flight data replay runs on. +REPLAY_CREDENTIALS = ( + pathlib.Path(__file__).parent / 'data' / 'credentials.json') + + +class StubServiceAccountCredentials: + """Minimal stand-in for google service_account.Credentials. + + Only implements the delegation surface the session uses. Notably it is + not a google.auth Scoped instance, so with_scopes_if_required leaves it + alone in Session.__init__. + """ + + def __init__(self, scopes=None, subject=None): + self.scopes = scopes + self.subject = subject + + def with_scopes(self, scopes): + return StubServiceAccountCredentials(scopes=scopes, subject=self.subject) + + def with_subject(self, subject): + return StubServiceAccountCredentials(scopes=self.scopes, subject=subject) + + +class StubCredentials: + """Credentials supporting neither scoping nor delegation.""" + + +def session_with_default_credentials(credentials): + """A session that got its credentials from google.auth.default. + + Passing them to Session directly takes a different path, one that + doesn't share the thread cached http. + """ + with mock.patch('google.auth.default', + return_value=(credentials, 'proj')): + return Session() + + +def service_account_session(): + """A session whose default credentials support delegation.""" + return session_with_default_credentials(StubServiceAccountCredentials()) + + +def test_get_workspace_customer_defaults_to_my_customer(monkeypatch): + monkeypatch.delenv('GOOGLE_WORKSPACE_CUSTOMER', raising=False) + assert get_workspace_customer() == 'my_customer' + + +def test_get_workspace_customer_env_override(monkeypatch): + monkeypatch.setenv('GOOGLE_WORKSPACE_CUSTOMER', 'C03abc123') + assert get_workspace_customer() == 'C03abc123' + + +def test_get_workspace_subject_unset(monkeypatch): + monkeypatch.delenv('GOOGLE_WORKSPACE_SUBJECT', raising=False) + assert get_workspace_subject() is None + + +def test_unscopable_credentials_are_left_alone(monkeypatch): + """The invariant the rest of the test suite depends on. + + Loads the committed credentials rather than whatever the developer has, + because the point is that this authorized_user credential supports + neither with_scopes nor with_subject. + """ + monkeypatch.delenv('GOOGLE_WORKSPACE_SUBJECT', raising=False) + credentials, _ = google.auth.load_credentials_from_file(REPLAY_CREDENTIALS) + session = session_with_default_credentials(credentials) + client = session.client( + 'admin', 'directory_v1', 'users', scopes=(WORKSPACE_SCOPE,)) + assert client._credentials is session._credentials + assert client._use_cached_http is True + + +def test_declared_scopes_are_requested(monkeypatch): + """Scopes are applied whether or not a subject is configured.""" + monkeypatch.delenv('GOOGLE_WORKSPACE_SUBJECT', raising=False) + session = service_account_session() + client = session.client( + 'admin', 'directory_v1', 'users', scopes=(WORKSPACE_SCOPE,)) + assert client._credentials.scopes == [WORKSPACE_SCOPE] + assert client._credentials.subject is None + + +def test_subject_impersonates(monkeypatch): + monkeypatch.setenv('GOOGLE_WORKSPACE_SUBJECT', 'admin@example.com') + session = service_account_session() + client = session.client( + 'admin', 'directory_v1', 'users', scopes=(WORKSPACE_SCOPE,)) + assert client._credentials.scopes == [WORKSPACE_SCOPE] + assert client._credentials.subject == 'admin@example.com' + # The thread cached http is authorized with the session's own + # credentials, so a client using different credentials must not share it. + assert client._use_cached_http is False + + +def test_subject_with_non_service_account_credentials_raises(monkeypatch): + monkeypatch.setenv('GOOGLE_WORKSPACE_SUBJECT', 'admin@example.com') + session = session_with_default_credentials(StubCredentials()) + with pytest.raises(ValueError) as ectx: + session.client( + 'admin', 'directory_v1', 'users', scopes=(WORKSPACE_SCOPE,)) + assert 'GOOGLE_WORKSPACE_SUBJECT' in str(ectx.value) + + +def test_clients_without_scopes_are_unaffected(monkeypatch): + monkeypatch.setenv('GOOGLE_WORKSPACE_SUBJECT', 'admin@example.com') + session = service_account_session() + client = session.client('compute', 'v1', 'instances') + assert client._credentials is session._credentials + assert client._use_cached_http is True diff --git a/tools/c7n_gcp/tests/test_query.py b/tools/c7n_gcp/tests/test_query.py index 5b7bcffa9e1..72c70f765e8 100644 --- a/tools/c7n_gcp/tests/test_query.py +++ b/tools/c7n_gcp/tests/test_query.py @@ -83,6 +83,7 @@ def test_gcp_resource_metadata_asset_type(): 'vertex-ai-publisher', # Synthetic resource (no asset inventory type) 'vertex-ai-publisher-model', # Catalog resource (no asset inventory type) 'vertex-ai-location', + 'workspace-user', # not a GCP resource (no asset inventory type) 'vertex-ai-evaluation-run', # No asset inventory type yet 'vertex-ai-metadata-store-artifact', # Not tracked by asset inventory )) diff --git a/tools/c7n_gcp/tests/test_resource.py b/tools/c7n_gcp/tests/test_resource.py index b05db72cc71..ee699cb100e 100644 --- a/tools/c7n_gcp/tests/test_resource.py +++ b/tools/c7n_gcp/tests/test_resource.py @@ -23,6 +23,7 @@ 'vertex-ai-publisher', # synthetic resource with no permissions 'vertex-ai-publisher-model', # catalog resource with no permissions 'vertex-ai-location', # metadata resource with no permissions + 'workspace-user', # OAuth scope and workspace roles, not GCP IAM )) diff --git a/tools/c7n_gcp/tests/test_workspace.py b/tools/c7n_gcp/tests/test_workspace.py new file mode 100644 index 00000000000..84b52e4deaf --- /dev/null +++ b/tools/c7n_gcp/tests/test_workspace.py @@ -0,0 +1,191 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +# The flight data here is recorded against a real Google Workspace. To set +# that up, or to re-record, see +# terraform/workspace_user_query/workspace-setup.md + +import json +import os +from unittest import mock + +import pytest +from googleapiclient import discovery_cache + +from c7n.config import Bag, Config +from c7n.exceptions import PolicyExecutionError +from c7n_gcp.resources.workspace import WorkspaceUser +from gcp_common import BaseTest +from terraform.workspace_user_query.workspace_sso import add_sso_assignment + + +class WorkspaceUserMetaTest(BaseTest): + + def get_manager(self): + policy = Bag({'name': 'workspace-users', + 'resource': 'gcp.workspace-user', + 'provider_name': 'gcp'}) + ctx = self.get_context(config=Config.empty(), policy=policy) + return WorkspaceUser(ctx, policy) + + def test_resource_query_defaults_to_my_customer(self): + self.assertEqual( + self.get_manager().get_resource_query(), + {'customer': 'my_customer'}) + + def test_resource_query_honors_customer_env(self): + with mock.patch.dict( + os.environ, {'GOOGLE_WORKSPACE_CUSTOMER': 'C03abc123'}): + self.assertEqual( + self.get_manager().get_resource_query(), + {'customer': 'C03abc123'}) + + def test_urn_has_no_project_or_location(self): + self.assertEqual( + WorkspaceUser.resource_type.get_urns( + [{'id': '100000000000000000003', + 'primaryEmail': 'test_no2sv@example.com'}], None), + ['gcp:admin:::workspace-user/100000000000000000003']) + + def test_declared_scopes_accepted_by_api(self): + """Workspace authorization is OAuth scopes plus Workspace admin + roles, so it can't be checked against GCP IAM permissions. Validate + the declared scopes against the API's own discovery document. + """ + rt = WorkspaceUser.resource_type + doc = json.loads(discovery_cache.get_static_doc(rt.service, rt.version)) + method = doc['resources'][rt.component]['methods'][rt.enum_spec[0]] + self.assertTrue(set(rt.scopes).issubset(method['scopes'])) + + +class WorkspaceUserQueryTest(BaseTest): + + def test_query(self): + factory = self.replay_flight_data('workspace-user-query') + policy = self.load_policy( + {'name': 'workspace-users', 'resource': 'gcp.workspace-user'}, + session_factory=factory) + resources = policy.run() + self.assertEqual(len(resources), 5) + # Exactly one super admin, without naming it. + self.assertEqual( + len([r for r in resources if r['isAdmin']]), 1) + self.assertEqual( + policy.resource_manager.get_urns(resources)[0], + 'gcp:admin:::workspace-user/100000000000000000001') + + def test_users_without_2sv(self): + factory = self.replay_flight_data('workspace-user-query') + policy = self.load_policy( + {'name': 'workspace-users-without-mfa', + 'resource': 'gcp.workspace-user', + 'filters': [ + {'type': 'value', + 'key': 'isEnrolledIn2Sv', + 'value': False}]}, + session_factory=factory) + self.assertEqual( + [r['primaryEmail'] for r in policy.run()], + ['test_no2sv@example.com', 'test_no2sv_susp@example.com']) + + def test_users_without_2sv_excluding_suspended(self): + """The policy as documented. + + Suspended users can't sign in, so a real policy excludes them. Both + clauses matter: test_no2sv_susp differs from test_no2sv only in being + suspended, so dropping the second clause would select it too. + """ + factory = self.replay_flight_data('workspace-user-query') + policy = self.load_policy( + {'name': 'workspace-users-without-mfa', + 'resource': 'gcp.workspace-user', + 'filters': [ + {'type': 'value', + 'key': 'isEnrolledIn2Sv', + 'value': False}, + {'type': 'value', + 'key': 'suspended', + 'value': False}]}, + session_factory=factory) + self.assertEqual( + [r['primaryEmail'] for r in policy.run()], + ['test_no2sv@example.com']) + + def test_delegated_admins_are_distinguishable(self): + """isAdmin covers super admins only, so policies about administrators + need isDelegatedAdmin too. Not a report field, but filterable. + """ + factory = self.replay_flight_data('workspace-user-query') + policy = self.load_policy( + {'name': 'workspace-delegated-admins', + 'resource': 'gcp.workspace-user', + 'filters': [ + {'type': 'value', + 'key': 'isDelegatedAdmin', + 'value': True}]}, + session_factory=factory) + self.assertEqual( + [r['primaryEmail'] for r in policy.run()], + ['test_admin@example.com']) + + +def test_workspace_user_state(test): + """Assert the recorded users match the table in + terraform/workspace_user_query/workspace-setup.md + + Everything else here depends on that state, so when this fails the table + says what to restore the workspace to before re-recording. The super + admin is keyed by its role rather than its name, which is tenant + specific. + """ + expected = { + '': (True, False, True, True, False, '/'), + 'test_admin': (False, True, True, True, True, '/'), + 'test_needno2sv': (False, False, True, False, True, + '/test-no-enforcement'), + 'test_no2sv': (False, False, False, False, False, + '/test-no-enforcement'), + 'test_no2sv_susp': (False, False, False, True, True, '/'), + } + fields = ('isAdmin', 'isDelegatedAdmin', 'isEnrolledIn2Sv', + 'isEnforcedIn2Sv', 'suspended', 'orgUnitPath') + + factory = test.replay_flight_data('workspace-user-query') + policy = test.load_policy( + {'name': 'workspace-users', 'resource': 'gcp.workspace-user'}, + session_factory=factory) + actual = { + ('' if r['isAdmin'] + else r['primaryEmail'].split('@')[0]): tuple(r[f] for f in fields) + for r in policy.run()} + assert actual == expected + + +def test_sso_assignments_are_rejected(test): + """Resolving which assignment governs a user isn't implemented, so the + 2sv fields can't be trusted when any assignment exists. + + terraform/workspace_user_query/workspace-setup.md covers what recording + this needs. + """ + factory = test.replay_flight_data('workspace-user-sso-assignment') + if test.recording: + add_sso_assignment(test) + policy = test.load_policy( + {'name': 'workspace-users', 'resource': 'gcp.workspace-user'}, + session_factory=factory) + with pytest.raises(PolicyExecutionError) as caught: + policy.run() + assert str(caught.value) == ( + "Organizations with SSO assignments aren't currently supported by" + " gcp.workspace-user.") + + +def test_workspace_user_report_fields(test): + factory = test.replay_flight_data('workspace-user-query') + policy = test.load_policy( + {'name': 'workspace-users', 'resource': 'gcp.workspace-user'}, + session_factory=factory) + resources = policy.run() + assert len(resources) == 5 + test.check_report_fields(policy, resources) From 27a1313fde4617019e4d923c275d1e9baf6dd136 Mon Sep 17 00:00:00 2001 From: Daniel Lindsley Date: Tue, 29 Sep 2026 12:20:42 -0500 Subject: [PATCH 17/65] azure - machine-learning-model-version - fetch anonymous resources (#11077) --- .../machine_learning_model_version.py | 3 +- ...arning_model_version_filter_anonymous.json | 142 ++++++++++++++++++ ...earning_model_version_filter_archived.json | 2 +- ..._machine_learning_model_version_query.json | 2 +- .../test_machine_learning_model_version.py | 16 ++ 5 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_anonymous.json diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_model_version.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_model_version.py index f786ba95db4..9a968c4f6ce 100644 --- a/tools/c7n_azure/c7n_azure/resources/machine_learning_model_version.py +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_model_version.py @@ -53,6 +53,7 @@ def enumerate_resources(self, parent_resource, type_info, vault_url=None, **para for version in client.model_versions.list( resource_group_name=resource_group, workspace_name=workspace_name, - name=container.name): + name=container.name, + list_view_type="All"): versions.append(version.serialize(True)) return versions diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_anonymous.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_anonymous.json new file mode 100644 index 00000000000..931b5f57bc5 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_anonymous.json @@ -0,0 +1,142 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2020-08-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest", + "name": "mlvvtest", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "properties": { + "friendlyName": "mlvvtest", + "provisioningState": "Succeeded" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model", + "name": "cctest-model", + "type": "Microsoft.MachineLearningServices/workspaces/models", + "properties": { + "description": "Cloud Custodian test model", + "latestVersion": "3" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions/1", + "name": "1", + "type": "Microsoft.MachineLearningServices/workspaces/models/versions", + "properties": { + "modelType": "CustomModel", + "modelUri": "azureml://locations/eastus/workspaces/mlvvtest/models/cctest-model/versions/1", + "isArchived": false, + "isAnonymous": false + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions/2", + "name": "2", + "type": "Microsoft.MachineLearningServices/workspaces/models/versions", + "properties": { + "modelType": "CustomModel", + "modelUri": "azureml://locations/eastus/workspaces/mlvvtest/models/cctest-model/versions/2", + "isArchived": false, + "isAnonymous": true + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions/3", + "name": "3", + "type": "Microsoft.MachineLearningServices/workspaces/models/versions", + "properties": { + "modelType": "CustomModel", + "modelUri": "azureml://locations/eastus/workspaces/mlvvtest/models/cctest-model/versions/3", + "isArchived": false, + "isAnonymous": true + } + } + ] + } + } + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_archived.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_archived.json index c78e4fa723a..98a774c6735 100644 --- a/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_archived.json +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_filter_archived.json @@ -80,7 +80,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_query.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_query.json index c78e4fa723a..98a774c6735 100644 --- a/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_query.json +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningModelVersionTest.test_machine_learning_model_version_query.json @@ -80,7 +80,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV/providers/Microsoft.MachineLearningServices/workspaces/mlvvtest/models/cctest-model/versions?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_model_version.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_model_version.py index 2618b9f4f76..9782585a8c0 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_model_version.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_model_version.py @@ -39,3 +39,19 @@ def test_machine_learning_model_version_filter_archived(self): resources = p.run() self.assertEqual(1, len(resources)) self.assertEqual('2', resources[0]['name']) + + @arm_template('machine-learning-model-version.json') + def test_machine_learning_model_version_filter_anonymous(self): + p = self.load_policy({ + 'name': 'find-anonymous-model-versions', + 'resource': 'azure.machine-learning-model-version', + 'filters': [{ + 'type': 'value', + 'key': 'properties.isAnonymous', + 'value': True + }], + }) + resources = p.run() + self.assertEqual(2, len(resources)) + self.assertTrue(all(r['properties']['isAnonymous'] for r in resources)) + self.assertEqual({'2', '3'}, {r['name'] for r in resources}) From d4d53ff4bbc975e0d132ce3432e3208af272f094 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 29 Sep 2026 11:25:07 -0600 Subject: [PATCH 18/65] azure - add machine learning data container archive action (#11063) --------- Co-authored-by: Claude Opus 5 --- .../c7n_azure/resources/machine_learning.py | 50 +- tools/c7n_azure/tests_azure/azure_common.py | 38 + ...chine-learning-data-container-archive.json | 784 ++++++++++++++++++ ...ning-data-container-archived-included.json | 625 ++++++++++++++ ...rTest.machine-learning-data-container.json | 568 ++++++++++--- ...chine-learning-data-container-archive.json | 123 +++ .../tests_resources/test_machine_learning.py | 71 +- 7 files changed, 2135 insertions(+), 124 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archive.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archived-included.json create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-data-container-archive.json diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning.py b/tools/c7n_azure/c7n_azure/resources/machine_learning.py index 0b6ae9ef35c..df891103a05 100644 --- a/tools/c7n_azure/c7n_azure/resources/machine_learning.py +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning.py @@ -1,3 +1,4 @@ +from c7n_azure.actions.base import AzureBaseAction from c7n_azure.provider import resources from c7n_azure.resources.arm import ArmResourceManager, ChildArmResourceManager from c7n.utils import type_schema @@ -76,7 +77,7 @@ class MachineLearningDataContainer(ChildArmResourceManager): - type: value key: systemData.lastModifiedAt value_type: age - op: lt + op: gt value: 90 """ @@ -101,7 +102,8 @@ class resource_type(ChildArmResourceManager.resource_type): def extra_args(cls, parent_resource): return { 'resource_group_name': parent_resource['resourceGroup'], - 'workspace_name': parent_resource['name'] + 'workspace_name': parent_resource['name'], + 'list_view_type': 'All', } def augment(self, resources): @@ -109,3 +111,47 @@ def augment(self, resources): if 'id' in resource: resource['resourceGroup'] = ResourceIdParser.get_resource_group(resource['id']) return resources + + +@MachineLearningDataContainer.action_registry.register('archive') +class MachineLearningDataContainerArchiveAction(AzureBaseAction): + """Archive Machine Learning data containers. + + Archiving hides the container and its versions from the workspace's + active views. It does not delete the backing data. + + :example: + + .. code-block:: yaml + + policies: + - name: archive-stale-machine-learning-data-containers + resource: azure.machine-learning-data-container + filters: + - type: value + key: systemData.lastModifiedAt + value_type: age + op: gt + value: 90 + actions: + - type: archive + + """ + + schema = type_schema('archive') + + def _prepare_processing(self): + self.client = self.manager.get_client() + + def _process_resource(self, resource): + if resource['properties']['isArchived']: + return + + resource['properties']['isArchived'] = True + self.client.data_containers.create_or_update( + resource_group_name=resource['resourceGroup'], + workspace_name=ResourceIdParser.get_resource_name( + resource['c7n:parent-id']), + name=resource['name'], + body=resource, + ) diff --git a/tools/c7n_azure/tests_azure/azure_common.py b/tools/c7n_azure/tests_azure/azure_common.py index 221a8e429d6..2db6bdff189 100644 --- a/tools/c7n_azure/tests_azure/azure_common.py +++ b/tools/c7n_azure/tests_azure/azure_common.py @@ -157,6 +157,24 @@ def __init__(self, *args, **kwargs): super(AzureVCRBaseTest, self).__init__(*args, **kwargs) self.vcr_enabled = not strtobool(os.environ.get('C7N_FUNCTIONAL', 'no')) + def setUp(self): + super(AzureVCRBaseTest, self).setUp() + # is_playback() is only meaningful here: by cleanup time a recording + # run has written the cassette, making it indistinguishable from one. + if self.is_playback() and getattr( + getattr(self, self._testMethodName), 'strict_cassette', False): + # Registered after the cassette's own cleanup, so it runs first, + # while the cassette is still open. + self.addCleanup(self._assert_cassette_fully_played) + + def _assert_cassette_fully_played(self): + unplayed = [ + '{0} {1}'.format(r.method, r.uri) + for i, r in enumerate(self.cassette.requests) + if not self.cassette.play_counts[i] + ] + self.assertFalse(unplayed, 'unplayed cassette interactions') + def is_playback(self): # You can't do this in setup because it is actually required by the base class # setup (via our callbacks), but it is also not possible to do until the base class setup @@ -540,6 +558,26 @@ def decorator(func): return decorator +def strict_cassette(name): + """Name the cassette, and require every recorded interaction to be played. + + ``cassette_name`` constrains a test from one side only: playback rejects a + request that was never recorded, but stays silent about a recorded request + the code under test stops making. A test whose subject *is* a request -- + the PUT or POST an action issues -- therefore passes just as happily when + the action does nothing at all, because its other assertions read recorded + responses either way. + + Mutually exclusive with sharing a cassette between tests, since a shared + recording is the union of what each of them replays. + """ + def decorator(func): + func.cassette_name = name + func.strict_cassette = True + return func + return decorator + + def requires_arm_polling(cls): orig_init = cls.__init__ # Make copy of original __init__, so we can call it without recursion diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archive.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archive.json new file mode 100644 index 00000000000..4a87d8ab352 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archive.json @@ -0,0 +1,784 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:27 GMT" + ], + "cache-control": [ + "no-cache" + ], + "content-length": [ + "10275" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: B22C7766F4FA4F6CA14B6CA823580FAF Ref B: CYS013050702051 Ref C: 2026-09-11T16:13:27Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-01T16:38:42.7908440Z", + "notebookInfo": { + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-25T13:03:18.7677055Z", + "notebookInfo": { + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849", + "name": "cctest-mlws-dc132679491849", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-ml-data-container-archive", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Storage/storageAccounts/cctestdcst132679491849", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Keyvault/vaults/cctestdckv132679491849", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.insights/components/cctestdcai132679491849", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T16:12:39.3270644Z", + "notebookInfo": { + "resourceId": "411de09bcf3246209fd9a3a82c16bb4a", + "fqdn": "ml-cctest-m-southcentralus-c348160f-3994-4f07-9c11-5928fef7c44c.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "c348160f-3994-4f07-9c11-5928fef7c44c", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "f7b76693-40ab-420c-b63d-64023195a7fb", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T16:12:39.3270644Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:12:39.3270644Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1659" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:28 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.397" + ], + "azureml-served-by-cluster": [ + "vienna-westus-01" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westus/0804704d-20f3-48c4-9cac-9aabc7480376" + ], + "x-msedge-ref": [ + "Ref A: 751AF943018446F8969A91C58040ACAF Ref B: CYS013050702019 Ref C: 2026-09-11T16:13:28Z" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20251201", + "name": "delete-test-20251201", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-13-29-0293802", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:20.0953074+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:20.1091032+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20260301", + "name": "delete-test-20260301", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-13-29-0294123", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:29.4355869+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:29.4449541+00:00" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:30 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.419" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/eastus/1e9dfb36-3634-420b-ae61-c1251cb9d6f4" + ], + "x-msedge-ref": [ + "Ref A: DFFB547486394D328CF18C6F20F19525 Ref B: CYS013050702011 Ref C: 2026-09-11T16:13:29Z" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:30 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.438" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/297db0cc-a4d8-47ad-adca-96ef3448b2c0" + ], + "x-msedge-ref": [ + "Ref A: 7514338B55AB4CDD910675DA38AD0BF2 Ref B: CYS013050702027 Ref C: 2026-09-11T16:13:30Z" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1846" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:31 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.163" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/343a699b-8f2b-4520-8039-9f014cb2fef3" + ], + "x-msedge-ref": [ + "Ref A: CD3805A9FDA240DEBD643A8043343B3E Ref B: CYS013050704021 Ref C: 2026-09-11T16:13:31Z" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-active", + "name": "cctest-dc-active", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-13-31-5792706", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:13:13.9565045+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:13:13.9676454+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-archived", + "name": "cctest-dc-archived", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": true, + "latestVersion": null, + "nextVersion": "2026-09-11-16-13-31-5792915", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:13:13.9623099+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:13:13.9703508+00:00" + } + } + ] + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-active?api-version=2023-04-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "content-length": [ + "867" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:31 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.213" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-ms-response-type": [ + "standard" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/a17c4e0b-defc-43a8-b0ed-88cab392d38c" + ], + "x-msedge-ref": [ + "Ref A: 2E2D5B257ED845DA84A70763DDD3FA27 Ref B: CYS013050703047 Ref C: 2026-09-11T16:13:31Z" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-active", + "name": "cctest-dc-active", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": true, + "latestVersion": null, + "nextVersion": "1", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:13:13.9565045+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:13:32.1194755+00:00", + "lastModifiedBy": "Jim Fulton", + "lastModifiedByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-active?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "867" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 16:13:32 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.053" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/57aed3d1-f098-4f33-8ae7-963ac8dd1dcc" + ], + "x-msedge-ref": [ + "Ref A: CD5469B9C7844A2885F53D3A95900BF4 Ref B: CYS013050701017 Ref C: 2026-09-11T16:13:33Z" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc132679491849/data/cctest-dc-active", + "name": "cctest-dc-active", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": true, + "latestVersion": null, + "nextVersion": "1", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:13:13.9565045+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:13:32.1194755+00:00", + "lastModifiedBy": "Jim Fulton", + "lastModifiedByType": "User" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archived-included.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archived-included.json new file mode 100644 index 00000000000..b697b5f6a73 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container-archived-included.json @@ -0,0 +1,625 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "cache-control": [ + "no-cache" + ], + "date": [ + "Fri, 11 Sep 2026 16:19:16 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "10273" + ], + "x-msedge-ref": [ + "Ref A: 5098CBF3FF244C5E8CEB9D23D5EC22A6 Ref B: CYS013050704019 Ref C: 2026-09-11T16:19:16Z" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-01T16:38:42.7908440Z", + "notebookInfo": { + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-25T13:03:18.7677055Z", + "notebookInfo": { + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988", + "name": "cctest-mlws-dc483846084988", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-ml-data-container-archive", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Storage/storageAccounts/cctestdcst483846084988", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Keyvault/vaults/cctestdckv483846084988", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.insights/components/cctestdcai483846084988", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T16:18:27.3055990Z", + "notebookInfo": { + "resourceId": "ccbb290a21af41b6810dcaa3522064de", + "fqdn": "ml-cctest-m-southcentralus-f4363e9a-9ad6-4e78-8f7b-e049a7d08a44.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "f4363e9a-9ad6-4e78-8f7b-e049a7d08a44", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "16853c90-7f7c-4577-b135-475916acfb7d", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T16:18:27.305599Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:18:27.305599Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1659" + ], + "x-request-time": [ + "0.103" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "date": [ + "Fri, 11 Sep 2026 16:19:18 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-westus-01" + ], + "x-msedge-ref": [ + "Ref A: 1DAB6D052A7B4E5B9723A09530389652 Ref B: CYS013050703053 Ref C: 2026-09-11T16:19:17Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westus/e27b17a1-440a-430a-ae74-c9321be94a08" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20251201", + "name": "delete-test-20251201", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-19-18-4454522", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:20.0953074+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:20.1091032+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20260301", + "name": "delete-test-20260301", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-19-18-4454629", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:29.4355869+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:29.4449541+00:00" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "x-request-time": [ + "0.381" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "date": [ + "Fri, 11 Sep 2026 16:19:18 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "x-msedge-ref": [ + "Ref A: D022E684B80A48E18057DE3A4BC29AF8 Ref B: CYS013050701051 Ref C: 2026-09-11T16:19:18Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/eastus/43a432a3-f657-406b-a079-a793e0081037" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "x-request-time": [ + "0.116" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "date": [ + "Fri, 11 Sep 2026 16:19:19 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-msedge-ref": [ + "Ref A: F10C295460204534ABFB2D670EBCB6A1 Ref B: CYS013050704011 Ref C: 2026-09-11T16:19:19Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westcentralus/0171f6ff-6210-419e-b108-70b970df2422" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1845" + ], + "x-request-time": [ + "0.203" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "date": [ + "Fri, 11 Sep 2026 16:19:20 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-msedge-ref": [ + "Ref A: C1BC697C435A49B88CFBC83C8EDA505E Ref B: CYS013050703037 Ref C: 2026-09-11T16:19:20Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/a3246830-6a5e-4d45-b1da-1333c965aa2c" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data/cctest-dc-active", + "name": "cctest-dc-active", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-19-20-5049655", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:19:01.9700619+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:19:01.979322+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data/cctest-dc-archived", + "name": "cctest-dc-archived", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": true, + "latestVersion": null, + "nextVersion": "2026-09-11-16-19-20-5049779", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:19:02.0266035+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:19:02.0364923+00:00" + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container.json index 36b178504fb..1aea666b2b9 100644 --- a/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container.json +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningDataContainerTest.machine-learning-data-container.json @@ -14,48 +14,115 @@ "message": "OK" }, "headers": { - "content-length": [ - "7661" - ], - "x-msedge-ref": [ - "Ref A: 0DA654A2C34D46E5A14ED1CAA6A109FB Ref B: CHI261140513052 Ref C: 2026-07-20T16:40:44Z" - ], - "cache-control": [ - "no-cache" - ], - "x-ms-ratelimit-remaining-subscription-global-reads": [ - "3749" - ], "content-type": [ "application/json; charset=utf-8" ], + "date": [ + "Fri, 11 Sep 2026 16:21:29 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], "x-ms-original-request-ids": [ + "", "", "" ], - "date": [ - "Mon, 20 Jul 2026 16:40:44 GMT" + "content-length": [ + "12815" ], - "x-cache": [ - "CONFIG_NOCACHE" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: 11013DD7D7FE4EFD81E8F832BA140615 Ref B: CYS013050702019 Ref C: 2026-09-11T16:21:29Z" ] }, "body": { "data": { "value": [ { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws", - "name": "cctest-mlws", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", "type": "Microsoft.MachineLearningServices/workspaces", "location": "eastus", "tags": {}, "etag": null, "properties": { - "friendlyName": "cctest-mlws", + "friendlyName": "", "description": "", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Storage/storageAccounts/cctestmlws320870742023", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Keyvault/vaults/cctestmlws320870742023", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.insights/components/cctestmlws320870742023", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -66,32 +133,31 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-07-06T16:29:09.1733326Z", + "creationTime": "2026-09-01T16:38:42.7908440Z", "notebookInfo": { - "resourceId": "cbc630e6300c4c90ad3ed7ded35adb19", - "fqdn": "ml-cctest-mlws-eastus-612f3069-9121-470d-9fec-d93aaac22974.eastus.notebooks.azure.net", + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "612f3069-9121-470d-9fec-d93aaac22974", + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", "discoveryUrl": "https://eastus.api.azureml.ms/discovery", - "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws", - "sdkTelemetryAppInsightsKey": "f5784ccd-178d-4ecc-9998-b05841b44ae9", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "dd21cedf-09fd-4a81-88a5-b6a90e9a7500", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -100,27 +166,27 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-07-06T16:29:09.1733326Z", + "createdAt": "2026-09-01T16:38:42.790844Z", "createdBy": "mock-user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-07-06T16:29:09.1733326Z", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", "lastModifiedBy": "mock-user@example.com", "lastModifiedByType": "User" } }, { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws", - "name": "cctest-mlws", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", "type": "Microsoft.MachineLearningServices/workspaces", - "location": "eastus", + "location": "southcentralus", "tags": {}, "etag": null, "properties": { - "friendlyName": "cctest-mlws", + "friendlyName": "cctest-mlws-cancel", "description": "", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.Storage/storageAccounts/cctestmlws874941179286", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.Keyvault/vaults/cctestmlws874941179286", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.insights/components/cctestmlws874941179286", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -131,32 +197,95 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-07-09T17:25:00.0839214Z", + "creationTime": "2026-08-25T13:03:18.7677055Z", "notebookInfo": { - "resourceId": "1b245c57f869445daed9826e3d8c84a8", - "fqdn": "ml-cctest-mlws-eastus-2278912a-4b83-44cf-af98-eeb1446fe6b7.eastus.notebooks.azure.net", + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "2278912a-4b83-44cf-af98-eeb1446fe6b7", + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", - "discoveryUrl": "https://eastus.api.azureml.ms/discovery", - "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws", - "sdkTelemetryAppInsightsKey": "f5784ccd-178d-4ecc-9998-b05841b44ae9", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "mock-user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "mock-user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988", + "name": "cctest-mlws-dc483846084988", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-ml-data-container-archive", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Storage/storageAccounts/cctestdcst483846084988", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.Keyvault/vaults/cctestdckv483846084988", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.insights/components/cctestdcai483846084988", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T16:18:27.3055990Z", + "notebookInfo": { + "resourceId": "ccbb290a21af41b6810dcaa3522064de", + "fqdn": "ml-cctest-m-southcentralus-f4363e9a-9ad6-4e78-8f7b-e049a7d08a44.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "f4363e9a-9ad6-4e78-8f7b-e049a7d08a44", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "674080ab-710e-4296-af48-71c90ba06edb", + "principalId": "16853c90-7f7c-4577-b135-475916acfb7d", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -165,17 +294,17 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-07-09T17:25:00.0839214Z", + "createdAt": "2026-09-11T16:18:27.305599Z", "createdBy": "mock-user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-07-09T17:25:00.0839214Z", + "lastModifiedAt": "2026-09-11T16:18:27.305599Z", "lastModifiedBy": "mock-user@example.com", "lastModifiedByType": "User" } }, { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml438669734316", - "name": "cctestml438669734316", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml591895132702", + "name": "cctestml591895132702", "type": "Microsoft.MachineLearningServices/workspaces", "location": "southcentralus", "tags": {}, @@ -183,9 +312,9 @@ "properties": { "friendlyName": "cctest-machine-learning-workspace", "description": "", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.Storage/storageAccounts/cctestmlst438669734316", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.Keyvault/vaults/cctestmlkv438669734316", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.insights/components/cctestmlai438669734316", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.Storage/storageAccounts/cctestmlst591895132702", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.Keyvault/vaults/cctestmlkv591895132702", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.insights/components/cctestmlai591895132702", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -196,32 +325,31 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-07-20T16:37:51.1808784Z", + "creationTime": "2026-09-11T16:20:18.6412940Z", "notebookInfo": { - "resourceId": "d3d32b1e3e6c4a76a50d44084228c856", - "fqdn": "ml-cctestml-southcentralus-1b25ce93-44bd-4363-9b47-512ff2fc42c7.southcentralus.notebooks.azure.net", + "resourceId": "4845617443774d858e0578b7d89edd7b", + "fqdn": "ml-cctestml-southcentralus-de76d53e-1060-4af5-9461-d49de11bf264.southcentralus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "1b25ce93-44bd-4363-9b47-512ff2fc42c7", + "workspaceId": "de76d53e-1060-4af5-9461-d49de11bf264", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", - "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml438669734316", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml591895132702", "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "783da398-b553-4313-a66c-7c35ab9e1e47", + "principalId": "eb1e048d-58ed-4222-82d9-0ac58c53ec38", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -230,10 +358,10 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-07-20T16:37:51.1808784Z", + "createdAt": "2026-09-11T16:20:18.641294Z", "createdBy": "mock-user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-07-20T16:37:51.1808784Z", + "lastModifiedAt": "2026-09-11T16:20:18.641294Z", "lastModifiedBy": "mock-user@example.com", "lastModifiedByType": "User" } @@ -246,7 +374,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws/data?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -256,41 +384,139 @@ "message": "OK" }, "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Fri, 11 Sep 2026 16:21:30 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], "cache-control": [ "no-cache" ], - "date": [ - "Mon, 20 Jul 2026 16:40:45 GMT" + "x-msedge-ref": [ + "Ref A: FA63F6BD755C41E4BB42F17C78193AEF Ref B: CYS013050702009 Ref C: 2026-09-11T16:21:30Z" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.096" ], "content-length": [ - "17" + "1659" + ], + "azureml-served-by-cluster": [ + "vienna-westus-01" ], "x-ms-ratelimit-remaining-subscription-global-reads": [ "3749" ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westus/9998ab7d-0478-42b6-94f5-900cf4f7b9e0" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20251201", + "name": "delete-test-20251201", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-21-30-8843508", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:20.0953074+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:20.1091032+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/data/delete-test-20260301", + "name": "delete-test-20260301", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "API deletion test", + "tags": null, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-21-30-8843600", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-08-12T14:05:29.4355869+00:00", + "createdBy": "Drea Jenkins", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:05:29.4449541+00:00" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { "content-type": [ "application/json; charset=utf-8" ], - "request-context": [ - "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + "date": [ + "Fri, 11 Sep 2026 16:21:31 GMT" ], "x-cache": [ "CONFIG_NOCACHE" ], - "x-request-time": [ - "0.485" + "cache-control": [ + "no-cache" ], "x-msedge-ref": [ - "Ref A: 1661AE8D846C4BC8B7C755CAD01BD219 Ref B: CHI261140510060 Ref C: 2026-07-20T16:40:45Z" + "Ref A: 8A642F75E20D48E09E55970146F87DA9 Ref B: CYS013050702023 Ref C: 2026-09-11T16:21:31Z" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.075" + ], + "content-length": [ + "17" ], "azureml-served-by-cluster": [ "vienna-eastus-01" ], - "x-ms-operation-identifier": [ - "mock-operation-identifier" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" ], - "x-ms-response-type": [ - "standard" + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/eastus/0ab1e95f-f53a-4f83-bf7b-b2ba31db4aa2" ] }, "body": { @@ -303,7 +529,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-online-endpoint/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws/data?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/data?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -313,41 +539,41 @@ "message": "OK" }, "headers": { - "cache-control": [ - "no-cache" + "content-type": [ + "application/json; charset=utf-8" ], "date": [ - "Mon, 20 Jul 2026 16:40:46 GMT" + "Fri, 11 Sep 2026 16:21:31 GMT" ], - "content-length": [ - "17" + "x-cache": [ + "CONFIG_NOCACHE" ], - "x-ms-ratelimit-remaining-subscription-global-reads": [ - "3749" + "cache-control": [ + "no-cache" ], - "content-type": [ - "application/json; charset=utf-8" + "x-msedge-ref": [ + "Ref A: 8E16BD7C3D424611AC2BE2C370A9B548 Ref B: CYS013050701011 Ref C: 2026-09-11T16:21:31Z" + ], + "x-ms-response-type": [ + "standard" ], "request-context": [ "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], - "x-cache": [ - "CONFIG_NOCACHE" - ], "x-request-time": [ - "0.560" + "0.053" ], - "x-msedge-ref": [ - "Ref A: 8DA1A33964364AA7AD1A4724678C66AC Ref B: CHI261140505062 Ref C: 2026-07-20T16:40:46Z" + "content-length": [ + "17" ], "azureml-served-by-cluster": [ - "vienna-eastus-02" + "vienna-southcentralus-02" ], - "x-ms-operation-identifier": [ - "mock-operation-identifier" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" ], - "x-ms-response-type": [ - "standard" + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/ee5a8017-1066-44da-9d07-219fdc0b3f33" ] }, "body": { @@ -360,7 +586,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml438669734316/data?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -370,48 +596,150 @@ "message": "OK" }, "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Fri, 11 Sep 2026 16:21:32 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], "cache-control": [ "no-cache" ], - "date": [ - "Mon, 20 Jul 2026 16:40:47 GMT" + "x-msedge-ref": [ + "Ref A: 1912B351FB474C33A376F63300F4CB65 Ref B: CYS013050701009 Ref C: 2026-09-11T16:21:32Z" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.136" ], "content-length": [ - "910" + "1845" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" ], "x-ms-ratelimit-remaining-subscription-global-reads": [ "3749" ], + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/fd73749f-3894-4aa5-ba28-3ddead1c23a9" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data/cctest-dc-active", + "name": "cctest-dc-active", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": false, + "latestVersion": null, + "nextVersion": "2026-09-11-16-21-32-4027759", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:19:01.9700619+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:19:01.979322+00:00" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-data-container-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-dc483846084988/data/cctest-dc-archived", + "name": "cctest-dc-archived", + "type": "Microsoft.MachineLearningServices/workspaces/data", + "properties": { + "description": "Archived fixture data container for archive action tests.", + "tags": { + "custodian": "test" + }, + "properties": null, + "isArchived": true, + "latestVersion": null, + "nextVersion": "2026-09-11-16-21-32-4027849", + "dataType": "uri_file" + }, + "systemData": { + "createdAt": "2026-09-11T16:19:02.0266035+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T16:19:02.0364923+00:00" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml591895132702/data?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { "content-type": [ "application/json; charset=utf-8" ], - "request-context": [ - "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + "date": [ + "Fri, 11 Sep 2026 16:21:32 GMT" ], "x-cache": [ "CONFIG_NOCACHE" ], - "x-request-time": [ - "0.320" + "cache-control": [ + "no-cache" ], "x-msedge-ref": [ - "Ref A: ED462D664F1D4B33ACE4B170EC5FF4C5 Ref B: CH1AA2020603029 Ref C: 2026-07-20T16:40:47Z" + "Ref A: 19D6C69155824BEBBD807A962D278B49 Ref B: CYS013050702017 Ref C: 2026-09-11T16:21:32Z" + ], + "x-ms-response-type": [ + "standard" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.146" + ], + "content-length": [ + "908" ], "azureml-served-by-cluster": [ "vienna-southcentralus-02" ], - "x-ms-operation-identifier": [ - "mock-operation-identifier" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" ], - "x-ms-response-type": [ - "standard" + "x-ms-operation-identifier": [ + "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/684e72bf-8187-42f9-a653-e2b75e58a35d" ] }, "body": { "data": { "value": [ { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml438669734316/data/cctest-ml-data-container", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning/providers/Microsoft.MachineLearningServices/workspaces/cctestml591895132702/data/cctest-ml-data-container", "name": "cctest-ml-data-container", "type": "Microsoft.MachineLearningServices/workspaces/data", "properties": { @@ -422,14 +750,14 @@ "properties": null, "isArchived": false, "latestVersion": null, - "nextVersion": "2026-07-20-16-40-47-9429928", + "nextVersion": "2026-09-11-16-21-32-9493508", "dataType": "uri_file" }, "systemData": { - "createdAt": "2026-07-20T16:38:26.5674462+00:00", - "createdBy": "mock-user@example.com", + "createdAt": "2026-09-11T16:21:08.2693782+00:00", + "createdBy": "Jim Fulton", "createdByType": "User", - "lastModifiedAt": "2026-07-20T16:38:26.5981153+00:00" + "lastModifiedAt": "2026-09-11T16:21:08.2799133+00:00" } } ] @@ -438,4 +766,4 @@ } } ] -} +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-data-container-archive.json b/tools/c7n_azure/tests_azure/templates/machine-learning-data-container-archive.json new file mode 100644 index 00000000000..4351b5e8336 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-data-container-archive.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + } + }, + "variables": { + "workspaceName": "[concat('cctest-mlws-dc', parameters('uniqueId'))]", + "activeDataContainerName": "cctest-dc-active", + "archivedDataContainerName": "cctest-dc-archived", + "storageName": "[concat('cctestdcst', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestdckv', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestdcai', parameters('uniqueId'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-10-01", + "name": "[variables('keyVaultName')]", + "location": "[resourceGroup().location]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [], + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[resourceGroup().location]", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "properties": { + "friendlyName": "cctest-ml-data-container-archive", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", + "publicNetworkAccess": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/data", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('activeDataContainerName'))]", + "properties": { + "description": "Non-archived fixture data container for archive action tests.", + "dataType": "uri_file", + "isArchived": false, + "properties": { + "custodian": "true" + }, + "tags": { + "custodian": "test" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/data", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('archivedDataContainerName'))]", + "properties": { + "description": "Archived fixture data container for archive action tests.", + "dataType": "uri_file", + "isArchived": true, + "properties": { + "custodian": "true" + }, + "tags": { + "custodian": "test" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py index 332ae400acf..2a2576e8615 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py @@ -7,7 +7,15 @@ SystemData ) -from ..azure_common import BaseTest, arm_template, cassette_name +from c7n.utils import local_session +from c7n_azure.session import Session +from c7n_azure.utils import ResourceIdParser +from ..azure_common import ( + BaseTest, + arm_template, + cassette_name, + strict_cassette, + ) class MachineLearningWorkspaceTest(BaseTest): @@ -106,6 +114,13 @@ def test_machine_learning_data_container_schema_validate(self): }, validate=True) assert p + p = self.load_policy({ + 'name': 'archive-machine-learning-data-containers', + 'resource': 'azure.machine-learning-data-container', + 'actions': [{'type': 'archive'}] + }, validate=True) + assert p + @arm_template('machine-learning.json') @cassette_name('machine-learning-data-container') def test_machine_learning_data_container_policy_run(self): @@ -131,6 +146,57 @@ def test_machine_learning_data_container_policy_run(self): assert resources[0]['properties']['isArchived'] is False assert 'systemData' in resources[0] + @arm_template('machine-learning-data-container-archive.json') + @strict_cassette('machine-learning-data-container-archive') + def test_machine_learning_data_container_archive(self): + p = self.load_policy({ + 'name': 'archive-stale-ml-data-containers', + 'resource': 'azure.machine-learning-data-container', + 'filters': [{ + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-data-container-archive' + }], + 'actions': [{'type': 'archive'}] + }, validate=True, session_factory=Session) + + resources = p.run() + assert sorted(r['name'] for r in resources) == [ + 'cctest-dc-active', + 'cctest-dc-archived', + ] + + active = next(r for r in resources if r['name'] == 'cctest-dc-active') + client = local_session(Session).client( + 'azure.mgmt.machinelearningservices.MachineLearningServicesMgmtClient') + container = client.data_containers.get( + active['resourceGroup'], + ResourceIdParser.get_resource_name(active['c7n:parent-id']), + active['name'], + ) + assert container.properties.is_archived + + @arm_template('machine-learning-data-container-archive.json') + @cassette_name('machine-learning-data-container-archived-included') + def test_machine_learning_data_container_query_includes_archived(self): + p = self.load_policy({ + 'name': 'find-cctest-archive-fixture-data-containers', + 'resource': 'azure.machine-learning-data-container', + 'filters': [{ + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-data-container-archive' + }] + }) + resources = p.run() + assert { + r['name']: r['properties']['isArchived'] + for r in resources + } == { + 'cctest-dc-active': False, + 'cctest-dc-archived': True, + } + def test_machine_learning_data_container_child_query(self): parent_id = ( '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV' @@ -174,7 +240,8 @@ def test_machine_learning_data_container_child_query(self): client.data_containers.list.assert_called_once_with( resource_group_name='VV', - workspace_name='vvmlwrkspc' + workspace_name='vvmlwrkspc', + list_view_type='All', ) assert len(resources) == 1 assert resources[0]['id'] == data_container_id From 2e6971f5c0fbf16c78107d8d791a7e3761030004 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 29 Sep 2026 11:29:04 -0600 Subject: [PATCH 19/65] azure - machine-learning-job - include archived resources (#11100) --- .../resources/machine_learning_job.py | 1 + ...gJobTest.machine-learning-job-archive.json | 876 +++-- ...LearningJobTest.machine-learning-jobs.json | 3110 ++++++++++++++++- .../templates/machine-learning-job.json | 36 +- .../test_machine_learning_job.py | 56 +- 5 files changed, 3850 insertions(+), 229 deletions(-) diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py index fcaedf7855b..8a969d407f6 100644 --- a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py @@ -54,6 +54,7 @@ def extra_args(cls, parent_resource): return { 'resource_group_name': parent_resource['resourceGroup'], 'workspace_name': parent_resource['name'], + 'list_view_type': 'All', } diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-archive.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-archive.json index d0dbcd30e6c..69ba075deb3 100644 --- a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-archive.json +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-archive.json @@ -14,48 +14,115 @@ "message": "OK" }, "headers": { - "content-length": [ - "7785" + "date": [ + "Fri, 11 Sep 2026 18:01:51 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" ], "x-ms-original-request-ids": [ + "", "", "" ], - "content-type": [ - "application/json; charset=utf-8" - ], "x-msedge-ref": [ - "Ref A: 3BF356F9D17F4083AA76942777C51FD4 Ref B: CYS013050701047 Ref C: 2026-08-13T19:25:53Z" - ], - "date": [ - "Thu, 13 Aug 2026 19:25:53 GMT" - ], - "cache-control": [ - "no-cache" + "Ref A: E019BBB781D442FBA5901214A4BD462C Ref B: CYS013050701033 Ref C: 2026-09-11T18:01:51Z" ], - "x-cache": [ - "CONFIG_NOCACHE" + "content-length": [ + "12721" ], - "x-ms-ratelimit-remaining-subscription-global-reads": [ - "3749" + "content-type": [ + "application/json; charset=utf-8" ] }, "body": { "data": { "value": [ { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-dc-drea-5cd4", - "name": "c7n-ml-dc-drea-5cd4", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", "type": "Microsoft.MachineLearningServices/workspaces", "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", "tags": {}, "etag": null, "properties": { "friendlyName": "", "description": "", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.Storage/storageAccounts/c7nmldc5cd4", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.Keyvault/vaults/c7n-ml-dc-kv-drea-5cd4", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.insights/components/c7n-ml-dc-ai-drea-5cd4", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -66,32 +133,31 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-08-11T15:14:07.9964944Z", + "creationTime": "2026-09-01T16:38:42.7908440Z", "notebookInfo": { - "resourceId": "5122454c6ce5437c879f75c63406ecb9", - "fqdn": "ml-c7n-ml-dc-drea-5-westus-3585ef7b-4a23-465f-b0a4-fefa6e09788e.westus.notebooks.azure.net", + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "3585ef7b-4a23-465f-b0a4-fefa6e09788e", + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", - "discoveryUrl": "https://westus.api.azureml.ms/discovery", - "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-dc-drea-5cd4", - "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "788d423b-20f4-4e64-86db-04ce7ee1b5c0", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -100,29 +166,91 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-08-11T15:14:07.9964944Z", - "createdBy": "test@example.com", + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-08-11T15:14:07.9964944Z", - "lastModifiedBy": "test@example.com", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "user@example.com", "lastModifiedByType": "User" } }, { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", - "name": "aml-delete-0812095033", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "name": "cctest-mlws-369794175432", "type": "Microsoft.MachineLearningServices/workspaces", - "location": "westus", - "tags": { - "createdByToolkit": "cli-v2-1.34.1" + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-369794175432", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Storage/storageAccounts/cctestmlws369794175432", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Keyvault/vaults/cctestmlws369794175432", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.insights/components/cctestmlws369794175432", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T17:56:36.8565848Z", + "notebookInfo": { + "resourceId": "e405c1e0646c47a3855a31b12f7bc119", + "fqdn": "ml-cctest-mlws-3697-eastus-ac54ae34-e576-47c3-a000-42140773eba2.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "ac54ae34-e576-47c3-a000-42140773eba2", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false }, + "identity": { + "type": "SystemAssigned", + "principalId": "c1aa6797-caca-4874-822f-50867435584f", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T17:56:36.8565848Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T17:56:36.8565848Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, "etag": null, "properties": { - "friendlyName": "aml-delete-0812095033", - "description": "aml-delete-0812095033", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "friendlyName": "cctest-mlws-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -133,32 +261,31 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-08-12T14:03:06.5147287Z", + "creationTime": "2026-08-25T13:03:18.7677055Z", "notebookInfo": { - "resourceId": "67293dad521f4e8c82e6f3fe495b572e", - "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", - "discoveryUrl": "https://westus.api.azureml.ms/discovery", - "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", - "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -167,11 +294,11 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-08-12T14:03:06.5147287Z", - "createdBy": "test@example.com", + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", - "lastModifiedBy": "test@example.com", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "user@example.com", "lastModifiedByType": "User" } }, @@ -180,16 +307,14 @@ "name": "cctest-mlws-archive", "type": "Microsoft.MachineLearningServices/workspaces", "location": "southcentralus", - "tags": { - "createdByToolkit": "cli-v2-1.34.1" - }, + "tags": {}, "etag": null, "properties": { "friendlyName": "cctest-mlws-archive", - "description": "cctest-mlws-archive", - "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Storage/storageAccounts/cctestmlstorage901395176", - "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Keyvault/vaults/cctestmlkeyvault6c6a5fec", - "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.insights/components/cctestmlinsightsbf7c6786", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Storage/storageAccounts/cctestmlws370997388317", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Keyvault/vaults/cctestmlws370997388317", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.insights/components/cctestmlws370997388317", "hbiWorkspace": false, "tenantId": "00000000-0000-0000-0000-000000000003", "imageBuildCompute": null, @@ -200,32 +325,31 @@ "AllowOnlyApprovedOutbound" ], "isolationMode": "Disabled", - "enableNetworkMonitor": false, - "enableFirewallLog": false + "enableNetworkMonitor": false }, "allowRoleAssignmentOnRG": true, "v1LegacyMode": false, "softDeleteEnabled": false, - "creationTime": "2026-08-13T19:16:00.6798670Z", + "creationTime": "2026-09-11T17:57:24.3642512Z", "notebookInfo": { - "resourceId": "d105071bf88c4a7699fa376a22452ffe", - "fqdn": "ml-cctest-m-southcentralus-9cb7e579-f2cf-4f6d-97cb-47cd470d3295.southcentralus.notebooks.azure.net", + "resourceId": "620618d4f5684c53809be936e268c4a1", + "fqdn": "ml-cctest-m-southcentralus-3a1e121a-066d-444a-9a33-343a6ffee0b9.southcentralus.notebooks.azure.net", "isPrivateLinkEnabled": false, "notebookPreparationError": null }, "storageHnsEnabled": false, - "workspaceId": "9cb7e579-f2cf-4f6d-97cb-47cd470d3295", + "workspaceId": "3a1e121a-066d-444a-9a33-343a6ffee0b9", "linkedModelInventoryArmId": null, "privateLinkCount": 0, "publicNetworkAccess": "Enabled", "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive", - "sdkTelemetryAppInsightsKey": "d83d49eb-3973-4a30-937a-eb3fc2d37c24", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", "enableDataIsolation": false }, "identity": { "type": "SystemAssigned", - "principalId": "2bb14fea-f483-4816-b0f1-765c6b52852e", + "principalId": "8b1309c7-20bf-4253-a107-7d619901f274", "tenantId": "00000000-0000-0000-0000-000000000003" }, "kind": "Default", @@ -234,11 +358,11 @@ "tier": "Basic" }, "systemData": { - "createdAt": "2026-08-13T19:16:00.679867Z", - "createdBy": "test@example.com", + "createdAt": "2026-09-11T17:57:24.3642512Z", + "createdBy": "user@example.com", "createdByType": "User", - "lastModifiedAt": "2026-08-13T19:16:00.679867Z", - "lastModifiedBy": "test@example.com", + "lastModifiedAt": "2026-09-11T17:57:24.3642512Z", + "lastModifiedBy": "user@example.com", "lastModifiedByType": "User" } } @@ -250,7 +374,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev-westus/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-dc-drea-5cd4/jobs?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/jobs?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -260,26 +384,68 @@ "message": "OK" }, "headers": { + "date": [ + "Fri, 11 Sep 2026 18:01:52 GMT" + ], "content-length": [ "17" ], - "x-ms-operation-identifier": [ - "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westus/175e693d-1347-4012-b66b-b44e5683c2ae" + "azureml-served-by-cluster": [ + "vienna-westus-01" ], - "x-ms-response-type": [ - "standard" + "x-cache": [ + "CONFIG_NOCACHE" ], - "content-type": [ - "application/json; charset=utf-8" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" ], "x-msedge-ref": [ - "Ref A: 3DCB58AC795147DBAC85A00E836B48DC Ref B: CYS013050702017 Ref C: 2026-08-13T19:25:54Z" + "Ref A: F47C96C01E0849C499067E862B0A4CEF Ref B: CYS013050703051 Ref C: 2026-09-11T18:01:52Z" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], "cache-control": [ "no-cache" ], "x-request-time": [ - "0.078" + "0.072" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Fri, 11 Sep 2026 18:01:53 GMT" + ], + "content-length": [ + "17" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" ], "x-cache": [ "CONFIG_NOCACHE" @@ -287,14 +453,23 @@ "x-ms-ratelimit-remaining-subscription-global-reads": [ "3749" ], + "x-msedge-ref": [ + "Ref A: 8A4F7A801897446BB93CAD19EF2C462C Ref B: CYS013050701025 Ref C: 2026-09-11T18:01:53Z" + ], "request-context": [ "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], - "azureml-served-by-cluster": [ - "vienna-westus-01" + "cache-control": [ + "no-cache" ], - "date": [ - "Thu, 13 Aug 2026 19:25:54 GMT" + "x-request-time": [ + "0.277" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" ] }, "body": { @@ -307,7 +482,163 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/jobs?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Fri, 11 Sep 2026 18:01:53 GMT" + ], + "content-length": [ + "7480" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3748" + ], + "x-msedge-ref": [ + "Ref A: 75D1DCAA50BE416A9A2387E441FD898A Ref B: CYS013050701009 Ref C: 2026-09-11T18:01:54Z" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.520" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Completed", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -317,46 +648,194 @@ "message": "OK" }, "headers": { + "date": [ + "Fri, 11 Sep 2026 18:01:54 GMT" + ], "content-length": [ - "17" + "6805" ], - "x-ms-operation-identifier": [ - "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/westus/fe3283dd-c95f-4053-b3e7-a1d650016d93" + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" ], - "x-ms-response-type": [ - "standard" + "x-cache": [ + "CONFIG_NOCACHE" ], - "content-type": [ - "application/json; charset=utf-8" + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3748" ], "x-msedge-ref": [ - "Ref A: 63E8998D330F4E0D81BAE73A3A143926 Ref B: CYS013050704033 Ref C: 2026-08-13T19:25:55Z" + "Ref A: 788E7A3F54074FF8B8A94E7AD094F50B Ref B: CYS013050703031 Ref C: 2026-09-11T18:01:55Z" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], "cache-control": [ "no-cache" ], "x-request-time": [ - "0.062" - ], - "x-cache": [ - "CONFIG_NOCACHE" + "0.306" ], - "x-ms-ratelimit-remaining-subscription-global-reads": [ - "3749" - ], - "request-context": [ - "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" - ], - "azureml-served-by-cluster": [ - "vienna-westus-02" + "content-type": [ + "application/json; charset=utf-8" ], - "date": [ - "Thu, 13 Aug 2026 19:25:54 GMT" + "x-ms-response-type": [ + "standard" ] }, "body": { "data": { - "value": [] + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-running-job", + "name": "cctest-running-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:26:33", + "EndTimeUtc": "2026-08-25 13:26:56" + }, + "displayName": "cctest_running_job", + "status": "Canceled", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-running-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "sleep 600", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-running-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:23:30.5405095+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-completed-job", + "name": "cctest-completed-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:16:10", + "EndTimeUtc": "2026-08-25 13:16:19" + }, + "displayName": "cctest_completed_job", + "status": "Completed", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-completed-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-completed-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:04:17.9041614+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] } } } @@ -364,7 +843,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -374,26 +853,14 @@ "message": "OK" }, "headers": { - "content-length": [ - "3713" - ], - "x-ms-operation-identifier": [ - "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/0692b016-52e5-40d0-96be-41286d24ef85" - ], - "x-ms-response-type": [ - "standard" - ], - "content-type": [ - "application/json; charset=utf-8" - ], - "x-msedge-ref": [ - "Ref A: B3B9998C3076470FA7CA16E4518F81D4 Ref B: CYS013050703029 Ref C: 2026-08-13T19:25:55Z" + "date": [ + "Fri, 11 Sep 2026 18:01:55 GMT" ], - "cache-control": [ - "no-cache" + "content-length": [ + "3423" ], - "x-request-time": [ - "0.196" + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" ], "x-cache": [ "CONFIG_NOCACHE" @@ -401,14 +868,23 @@ "x-ms-ratelimit-remaining-subscription-global-reads": [ "3749" ], + "x-msedge-ref": [ + "Ref A: FDFA0626E2F14A24B34B96F375C9013B Ref B: CYS013050701033 Ref C: 2026-09-11T18:01:55Z" + ], "request-context": [ "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], - "azureml-served-by-cluster": [ - "vienna-southcentralus-01" + "cache-control": [ + "no-cache" ], - "date": [ - "Thu, 13 Aug 2026 19:25:55 GMT" + "x-request-time": [ + "0.199" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" ] }, "body": { @@ -424,16 +900,12 @@ "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" }, "properties": { - "mlflow.source.git.repoURL": "test@example.com", - "mlflow.source.git.branch": "jimfulton/feat/10949-azure-ml-job-archive", - "mlflow.source.git.commit": "c1ce0dce0b77346d223400d93f5f598eec874f2d", - "azureml.git.dirty": "False", "_azureml.ComputeTargetType": "amlctrain", "_azureml.ClusterName": "cctestcpuarchive", "ProcessInfoFile": "azureml-logs/process_info.json", "ProcessStatusFile": "azureml-logs/process_status.json", - "StartTimeUtc": "2026-08-13 19:19:44", - "EndTimeUtc": "2026-08-13 19:20:44" + "StartTimeUtc": "2026-09-11 18:01:09", + "EndTimeUtc": "2026-09-11 18:01:48" }, "displayName": "cctest_archive_job", "status": "Completed", @@ -484,11 +956,11 @@ }, "distribution": null, "limits": null, - "environmentVariables": {}, + "environmentVariables": null, "parameters": {} }, "systemData": { - "createdAt": "2026-08-13T19:17:35.6842089+00:00", + "createdAt": "2026-09-11T17:58:16.4423309+00:00", "createdBy": "Jim Fulton", "createdByType": "User" } @@ -511,44 +983,41 @@ "message": "OK" }, "headers": { - "content-length": [ - "3375" - ], - "x-ms-operation-identifier": [ - "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/a0b501a3-a941-4de5-9301-80fbac757ef1" - ], - "x-ms-response-type": [ - "standard" - ], - "x-ms-ratelimit-remaining-subscription-global-writes": [ - "2999" - ], "x-ms-ratelimit-remaining-subscription-writes": [ "199" ], - "content-type": [ - "application/json; charset=utf-8" - ], - "x-msedge-ref": [ - "Ref A: 36C22FD17F434C8FBE68E2617820DB55 Ref B: CYS013050701049 Ref C: 2026-08-13T19:25:56Z" + "date": [ + "Fri, 11 Sep 2026 18:01:56 GMT" ], - "cache-control": [ - "no-cache" + "content-length": [ + "3101" ], - "x-request-time": [ - "0.201" + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" ], "x-cache": [ "CONFIG_NOCACHE" ], + "x-msedge-ref": [ + "Ref A: 7C7B34D6C3064AF0A762E57AA1ACF2F0 Ref B: CYS013050702025 Ref C: 2026-09-11T18:01:56Z" + ], "request-context": [ "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], - "azureml-served-by-cluster": [ - "vienna-southcentralus-01" + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" ], - "date": [ - "Thu, 13 Aug 2026 19:25:56 GMT" + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.393" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" ] }, "body": { @@ -562,16 +1031,12 @@ "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" }, "properties": { - "mlflow.source.git.repoURL": "test@example.com", - "mlflow.source.git.branch": "jimfulton/feat/10949-azure-ml-job-archive", - "mlflow.source.git.commit": "c1ce0dce0b77346d223400d93f5f598eec874f2d", - "azureml.git.dirty": "False", "_azureml.ComputeTargetType": "amlctrain", "_azureml.ClusterName": "cctestcpuarchive", "ProcessInfoFile": "azureml-logs/process_info.json", "ProcessStatusFile": "azureml-logs/process_status.json", - "StartTimeUtc": "2026-08-13 19:19:44", - "EndTimeUtc": "2026-08-13 19:20:44" + "StartTimeUtc": "2026-09-11 18:01:09", + "EndTimeUtc": "2026-09-11 18:01:48" }, "displayName": "cctest_archive_job", "status": "Completed", @@ -622,11 +1087,11 @@ }, "distribution": null, "limits": null, - "environmentVariables": {}, + "environmentVariables": null, "parameters": {} }, "systemData": { - "createdAt": "2026-08-13T19:17:35.6842089+00:00", + "createdAt": "2026-09-11T17:58:16.4423309+00:00", "createdBy": "Jim Fulton", "createdByType": "User" } @@ -647,26 +1112,14 @@ "message": "OK" }, "headers": { - "content-length": [ - "3375" - ], - "x-ms-operation-identifier": [ - "tenantId=72d876e4-6b6f-4622-8566-8200130e81d4,objectId=e89404a5-c7db-4cdf-aed3-31daf7739325/southcentralus/28b8f101-74e9-4d53-951b-8014c6e3ace8" - ], - "x-ms-response-type": [ - "standard" - ], - "content-type": [ - "application/json; charset=utf-8" - ], - "x-msedge-ref": [ - "Ref A: 6A14A3BEB5D741B4A41BD310081516A1 Ref B: CYS013050704045 Ref C: 2026-08-13T19:25:57Z" + "date": [ + "Fri, 11 Sep 2026 18:01:57 GMT" ], - "cache-control": [ - "no-cache" + "content-length": [ + "3101" ], - "x-request-time": [ - "0.065" + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" ], "x-cache": [ "CONFIG_NOCACHE" @@ -674,14 +1127,23 @@ "x-ms-ratelimit-remaining-subscription-global-reads": [ "3749" ], + "x-msedge-ref": [ + "Ref A: 5DB99D1A6E6D4198953C37A3BBD84A52 Ref B: CYS013050702025 Ref C: 2026-09-11T18:01:57Z" + ], "request-context": [ "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" ], - "azureml-served-by-cluster": [ - "vienna-southcentralus-02" + "cache-control": [ + "no-cache" ], - "date": [ - "Thu, 13 Aug 2026 19:25:56 GMT" + "x-request-time": [ + "0.093" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" ] }, "body": { @@ -695,16 +1157,12 @@ "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" }, "properties": { - "mlflow.source.git.repoURL": "test@example.com", - "mlflow.source.git.branch": "jimfulton/feat/10949-azure-ml-job-archive", - "mlflow.source.git.commit": "c1ce0dce0b77346d223400d93f5f598eec874f2d", - "azureml.git.dirty": "False", "_azureml.ComputeTargetType": "amlctrain", "_azureml.ClusterName": "cctestcpuarchive", "ProcessInfoFile": "azureml-logs/process_info.json", "ProcessStatusFile": "azureml-logs/process_status.json", - "StartTimeUtc": "2026-08-13 19:19:44", - "EndTimeUtc": "2026-08-13 19:20:44" + "StartTimeUtc": "2026-09-11 18:01:09", + "EndTimeUtc": "2026-09-11 18:01:48" }, "displayName": "cctest_archive_job", "status": "Completed", @@ -755,11 +1213,11 @@ }, "distribution": null, "limits": null, - "environmentVariables": {}, + "environmentVariables": null, "parameters": {} }, "systemData": { - "createdAt": "2026-08-13T19:17:35.6842089+00:00", + "createdAt": "2026-09-11T17:58:16.4423309+00:00", "createdBy": "Jim Fulton", "createdByType": "User" } diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-jobs.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-jobs.json index 0369d60c487..d39817e0d37 100644 --- a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-jobs.json +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-jobs.json @@ -4,7 +4,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2020-08-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", "body": null, "headers": {} }, @@ -14,21 +14,356 @@ "message": "OK" }, "headers": { - "content-type": ["application/json; charset=utf-8"], - "cache-control": ["no-cache"] + "content-length": [ + "12721" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: 76CECCA419AA400E9F14F7DB48170054 Ref B: CYS013050703033 Ref C: 2026-09-11T17:58:33Z" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:33 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ] }, "body": { "data": { "value": [ { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws", - "name": "cctest-mlws", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", "type": "Microsoft.MachineLearningServices/workspaces", "location": "eastus", "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-01T16:38:42.7908440Z", + "notebookInfo": { + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "name": "cctest-mlws-369794175432", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-369794175432", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Storage/storageAccounts/cctestmlws369794175432", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Keyvault/vaults/cctestmlws369794175432", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.insights/components/cctestmlws369794175432", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T17:56:36.8565848Z", + "notebookInfo": { + "resourceId": "e405c1e0646c47a3855a31b12f7bc119", + "fqdn": "ml-cctest-mlws-3697-eastus-ac54ae34-e576-47c3-a000-42140773eba2.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "ac54ae34-e576-47c3-a000-42140773eba2", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "c1aa6797-caca-4874-822f-50867435584f", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T17:56:36.8565848Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T17:56:36.8565848Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, "properties": { - "friendlyName": "cctest-mlws", - "provisioningState": "Succeeded" + "friendlyName": "cctest-mlws-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-25T13:03:18.7677055Z", + "notebookInfo": { + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive", + "name": "cctest-mlws-archive", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-archive", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Storage/storageAccounts/cctestmlws370997388317", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Keyvault/vaults/cctestmlws370997388317", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.insights/components/cctestmlws370997388317", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T17:57:24.3642512Z", + "notebookInfo": { + "resourceId": "620618d4f5684c53809be936e268c4a1", + "fqdn": "ml-cctest-m-southcentralus-3a1e121a-066d-444a-9a33-343a6ffee0b9.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3a1e121a-066d-444a-9a33-343a6ffee0b9", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "8b1309c7-20bf-4253-a107-7d619901f274", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T17:57:24.3642512Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T17:57:24.3642512Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" } } ] @@ -39,7 +374,7 @@ { "request": { "method": "GET", - "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws/jobs?api-version=2023-04-01", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/jobs?api-version=2023-04-01&listViewType=All", "body": null, "headers": {} }, @@ -49,22 +384,2771 @@ "message": "OK" }, "headers": { - "content-type": ["application/json; charset=utf-8"], - "cache-control": ["no-cache"] + "content-length": [ + "17" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-westus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:35 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.479" + ], + "x-msedge-ref": [ + "Ref A: 8928D93CE08F4732A8FA824869B1C28D Ref B: CYS013050701017 Ref C: 2026-09-11T17:58:34Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:36 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.583" + ], + "x-msedge-ref": [ + "Ref A: D31DA57525A84016A17A52D214EFDC93 Ref B: CYS013050704047 Ref C: 2026-09-11T17:58:35Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7479" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:36 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.442" + ], + "x-msedge-ref": [ + "Ref A: 7A8FE8359DC44B6C87C1EA490AE4F318 Ref B: CYS013050704027 Ref C: 2026-09-11T17:58:36Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] }, "body": { "data": { "value": [ { - "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws/jobs/cctest-sweep-job", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", "name": "cctest-sweep-job", "type": "Microsoft.MachineLearningServices/workspaces/jobs", "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, "limits": { + "timeout": "P60D", "jobLimitsType": "Sweep", - "maxConcurrentTrials": 20 + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "6805" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:38 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.578" + ], + "x-msedge-ref": [ + "Ref A: A563CD32C5874798A8D298BBE80698A1 Ref B: CYS013050701025 Ref C: 2026-09-11T17:58:37Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-running-job", + "name": "cctest-running-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:26:33", + "EndTimeUtc": "2026-08-25 13:26:56" + }, + "displayName": "cctest_running_job", + "status": "Canceled", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-running-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "sleep 600", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-running-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:23:30.5405095+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-completed-job", + "name": "cctest-completed-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:16:10", + "EndTimeUtc": "2026-08-25 13:16:19" + }, + "displayName": "cctest_completed_job", + "status": "Completed", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-completed-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-completed-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:04:17.9041614+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "3324" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:38 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.222" + ], + "x-msedge-ref": [ + "Ref A: 7B23C2F002AD4CA2833E0B751C1EAC77 Ref B: CYS013050701009 Ref C: 2026-09-11T17:58:38Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs/cctest-archive-job", + "name": "cctest-archive-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpuarchive", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json" + }, + "displayName": "cctest_archive_job", + "status": "Queued", + "experimentName": "cctest_archive", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-archive-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-archive/workspaces/cctest-mlws-archive", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/computes/cctestcpuarchive", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/environments/cctest-archive-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-archive-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-11T17:58:16.4423309+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:58:40 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.113" + ], + "x-msedge-ref": [ + "Ref A: 27228FA4926746A6A5503B953564CA9E Ref B: CYS013050701021 Ref C: 2026-09-11T17:58:39Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:59:10 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.106" + ], + "x-msedge-ref": [ + "Ref A: E91A2402D5B74B61A2B050AEDE566BA2 Ref B: CYS013050704025 Ref C: 2026-09-11T17:59:10Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 17:59:41 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.095" + ], + "x-msedge-ref": [ + "Ref A: 70EEA6348E5A4314BF6096E8A3C67C82 Ref B: CYS013050704025 Ref C: 2026-09-11T17:59:41Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:00:11 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.089" + ], + "x-msedge-ref": [ + "Ref A: 24DED83DD2724471A2413B363EDE6B6B Ref B: CYS013050701017 Ref C: 2026-09-11T18:00:11Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:00:42 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.133" + ], + "x-msedge-ref": [ + "Ref A: 2EA7623A5ABF423A9D50959F6298666B Ref B: CYS013050703053 Ref C: 2026-09-11T18:00:42Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7054" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:12 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.123" + ], + "x-msedge-ref": [ + "Ref A: 479E3E17B10F4A7EAD8D7754A86E714A Ref B: CYS013050702011 Ref C: 2026-09-11T18:01:13Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Running", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7056" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:43 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.133" + ], + "x-msedge-ref": [ + "Ref A: DCC58FEFA3D6424885BF82B2F73FA633 Ref B: CYS013050703051 Ref C: 2026-09-11T18:01:43Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Completed", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job?api-version=2023-04-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7055" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:44 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.275" + ], + "x-msedge-ref": [ + "Ref A: AC043EB416624B19949AA1D24A18AC29 Ref B: CYS013050701037 Ref C: 2026-09-11T18:01:44Z" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Completed", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "12721" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: F2BE4EC04A1C4D53B91CE6F35FE94B9D Ref B: CYS013050701017 Ref C: 2026-09-11T18:01:45Z" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:45 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-01T16:38:42.7908440Z", + "notebookInfo": { + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "name": "cctest-mlws-369794175432", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-369794175432", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Storage/storageAccounts/cctestmlws369794175432", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Keyvault/vaults/cctestmlws369794175432", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.insights/components/cctestmlws369794175432", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T17:56:36.8565848Z", + "notebookInfo": { + "resourceId": "e405c1e0646c47a3855a31b12f7bc119", + "fqdn": "ml-cctest-mlws-3697-eastus-ac54ae34-e576-47c3-a000-42140773eba2.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "ac54ae34-e576-47c3-a000-42140773eba2", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "c1aa6797-caca-4874-822f-50867435584f", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T17:56:36.8565848Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T17:56:36.8565848Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "name": "cctest-mlws-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws970908088942", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws970908088942", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws970908088942", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-25T13:03:18.7677055Z", + "notebookInfo": { + "resourceId": "07bf556f17984f029aedf3d0f7162d7a", + "fqdn": "ml-cctest-m-southcentralus-6c4488bb-bb56-4bfe-b4f8-071635256c29.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "6c4488bb-bb56-4bfe-b4f8-071635256c29", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "848a7033-7d11-4b3e-80b0-2e6c0db0715c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-25T13:03:18.7677055Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-25T13:03:18.7677055Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive", + "name": "cctest-mlws-archive", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-archive", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Storage/storageAccounts/cctestmlws370997388317", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.Keyvault/vaults/cctestmlws370997388317", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.insights/components/cctestmlws370997388317", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-11T17:57:24.3642512Z", + "notebookInfo": { + "resourceId": "620618d4f5684c53809be936e268c4a1", + "fqdn": "ml-cctest-m-southcentralus-3a1e121a-066d-444a-9a33-343a6ffee0b9.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3a1e121a-066d-444a-9a33-343a6ffee0b9", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "8b1309c7-20bf-4253-a107-7d619901f274", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-11T17:57:24.3642512Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-11T17:57:24.3642512Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-westus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:46 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.062" + ], + "x-msedge-ref": [ + "Ref A: D37687AAAF784CC184C7061084D203EB Ref B: CYS013050702017 Ref C: 2026-09-11T18:01:46Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "17" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:47 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.266" + ], + "x-msedge-ref": [ + "Ref A: FA2FB659886C4CDA943B34310881311A Ref B: CYS013050701023 Ref C: 2026-09-11T18:01:46Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "7480" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:47 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.442" + ], + "x-msedge-ref": [ + "Ref A: B76B22020BC54CAA86AD7B13185500AF Ref B: CYS013050701009 Ref C: 2026-09-11T18:01:47Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/jobs/cctest-sweep-job", + "name": "cctest-sweep-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_max_concurrent_jobs": "20", + "_aml_system_max_total_jobs": "1", + "_aml_system_max_duration_minutes": "86400", + "_aml_system_policy_config": "{\"name\":\"Default\",\"properties\":{}}", + "_aml_system_generator_config": "{\"name\":\"RANDOM\",\"parameter_space\":{\"x\":\"choice(1)\"},\"properties\":{\"rule\":\"Random\",\"logbase\":null,\"seed\":null}}", + "_aml_system_primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "_aml_system_platform_config": "{\"ServiceAddress\":\"https://eastus.api.azureml.ms\",\"SubscriptionId\":\"ea42f556-5106-4743-99b0-c129bfa71a47\",\"ResourceGroupName\":\"test_machine-learning-job\",\"WorkspaceName\":\"cctest-mlws-369794175432\",\"ExperimentName\":\"Default\",\"Definition\":{\"Configuration\":null,\"Attribution\":null,\"TelemetryValues\":null,\"Overrides\":{\"Script\":null,\"Command\":\"echo loss: 1\",\"UseAbsolutePath\":true,\"Arguments\":[],\"SourceDirectoryDataStore\":null,\"Framework\":0,\"Target\":\"Serverless\",\"AutoClusterComputeSpecification\":{\"InstanceSize\":null,\"InstancePriority\":null,\"OsType\":null,\"Location\":null,\"RuntimeVersion\":null},\"DataReferences\":{},\"Data\":{},\"OutputData\":{},\"Datacaches\":[],\"JobName\":null,\"MaxRunDurationSeconds\":null,\"NodeCount\":1,\"InstanceTypes\":[],\"Priority\":null,\"CredentialPassthrough\":false,\"Identity\":null,\"Environment\":{\"Name\":\"cctest-sweep-environment\",\"Version\":\"1\",\"AssetId\":\"azureml://locations/eastus/workspaces/ac54ae34-e576-47c3-a000-42140773eba2/environments/cctest-sweep-environment/versions/1\",\"AutoRebuild\":false,\"Python\":{\"InterpreterPath\":null,\"UserManagedDependencies\":true,\"CondaDependencies\":null,\"BaseCondaEnvironment\":null},\"EnvironmentVariables\":{},\"Docker\":{\"BaseImage\":\"mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04\",\"Platform\":{\"Os\":\"Linux\",\"Architecture\":\"amd64\"},\"BaseDockerfile\":null,\"BaseImageRegistry\":null},\"Spark\":{\"Repositories\":[],\"Packages\":[],\"PrecachePackages\":true},\"InferencingStackVersion\":null},\"History\":{\"OutputCollection\":true,\"DirectoriesToWatch\":[\"logs\"],\"EnableMLflowTracking\":true},\"Spark\":{\"Configuration\":{}},\"ParallelTask\":{\"MaxRetriesPerWorker\":0,\"WorkerCountPerNode\":1,\"TerminalExitCodes\":null,\"Configuration\":{}},\"BatchAi\":{\"NodeCount\":0},\"AmlCompute\":{\"Name\":null,\"VmSize\":null,\"RetainCluster\":false,\"ClusterMaxNodeCount\":null},\"AISuperComputer\":{\"InstanceType\":\"D2\",\"FrameworkImage\":null,\"ImageVersion\":null,\"Location\":null,\"AISuperComputerStorageData\":null,\"Interactive\":false,\"ScalePolicy\":null,\"VirtualClusterArmId\":null,\"FoundryComputeArmId\":null,\"TensorboardLogDirectory\":null,\"SSHPublicKey\":null,\"SSHPublicKeys\":null,\"EnableAzmlInt\":true,\"Priority\":\"Medium\",\"SLATier\":\"Standard\",\"UserAlias\":null},\"KubernetesCompute\":{\"InstanceType\":null},\"Tensorflow\":{\"WorkerCount\":0,\"ParameterServerCount\":0},\"Mpi\":{\"ProcessCountPerNode\":0},\"PyTorch\":{\"CommunicationBackend\":null,\"ProcessCount\":null},\"Hdi\":{\"YarnDeployMode\":0},\"ContainerInstance\":{\"Region\":null,\"CpuCores\":2.0,\"MemoryGb\":3.5},\"ExposedPorts\":null,\"Docker\":{\"UseDocker\":true,\"SharedVolumes\":null,\"ShmSize\":\"2g\",\"Arguments\":null},\"Cmk8sCompute\":{\"Configuration\":{}},\"CommandReturnCodeConfig\":{\"ReturnCode\":0,\"SuccessfulReturnCodes\":[]},\"EnvironmentVariables\":{},\"ApplicationEndpoints\":{},\"Parameters\":[]},\"SnapshotId\":null,\"Snapshots\":[],\"SourceCodeDataReference\":null,\"ParentRunId\":null,\"DataContainerId\":null,\"RunType\":null,\"DisplayName\":null,\"EnvironmentAssetId\":null,\"Properties\":{},\"Tags\":{},\"AggregatedArtifactPath\":null},\"ParentRunId\":\"cctest-sweep-job\"}", + "_aml_system_resume_child_runs": "null", + "_aml_system_all_jobs_generated": "false", + "_aml_system_cancellation_requested": "false", + "_aml_system_samples_generated_count": "0" + }, + "properties": { + "primary_metric_config": "{\"name\":\"loss\",\"goal\":\"minimize\"}", + "resume_from": "null", + "runTemplate": "HyperDrive", + "azureml.runsource": "hyperdrive", + "platform": "AML", + "ContentSnapshotId": "", + "user_agent": "managementfrontend/c32456d9f64159263ebf2496dd683d44ce103a54+c32456d9f64159263ebf2496dd683d44ce103a54" + }, + "displayName": "good_owl_r9nhg6mf", + "status": "Completed", + "experimentName": "Default", + "services": { + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-sweep-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job/workspaces/cctest-mlws-369794175432", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/computes/Serverless", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Sweep", + "searchSpace": { + "x": "choice(1)" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random", + "seed": null, + "rule": "Random" + }, + "limits": { + "timeout": "P60D", + "jobLimitsType": "Sweep", + "maxTotalTrials": 1, + "maxConcurrentTrials": 20, + "trialTimeout": null + }, + "earlyTermination": null, + "objective": { + "primaryMetric": "loss", + "goal": "Minimize" + }, + "trial": { + "codeId": null, + "command": "echo loss: 1", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-369794175432/environments/cctest-sweep-environment/versions/1", + "environmentVariables": {}, + "distribution": null, + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": { + "ComputeSpecification": { + "Automatic": true + } + }, + "shmSize": "2g", + "dockerArgs": null + } + }, + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-sweep-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + } + }, + "systemData": { + "createdAt": "2026-09-11T17:57:13.2681408+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "6805" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:48 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.179" + ], + "x-msedge-ref": [ + "Ref A: 0B16364060864A53B9F2185059A5DADA Ref B: CYS013050703049 Ref C: 2026-09-11T18:01:48Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-running-job", + "name": "cctest-running-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:26:33", + "EndTimeUtc": "2026-08-25 13:26:56" + }, + "displayName": "cctest_running_job", + "status": "Canceled", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-running-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "sleep 600", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-running-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:23:30.5405095+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/jobs/cctest-completed-job", + "name": "cctest-completed-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-08-25 13:16:10", + "EndTimeUtc": "2026-08-25 13:16:19" + }, + "displayName": "cctest_completed_job", + "status": "Completed", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-completed-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/computes/cctestcpucancel", + "isArchived": true, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-completed-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-08-25T13:04:17.9041614+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "3423" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Fri, 11 Sep 2026 18:01:48 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-ms-response-type": [ + "standard" + ], + "x-request-time": [ + "0.150" + ], + "x-msedge-ref": [ + "Ref A: EF8389514FEE47E1A864B8D06B1B3B78 Ref B: CYS013050703051 Ref C: 2026-09-11T18:01:48Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/jobs/cctest-archive-job", + "name": "cctest-archive-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpuarchive", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-09-11 18:01:09", + "EndTimeUtc": "2026-09-11 18:01:48" + }, + "displayName": "cctest_archive_job", + "status": "Completed", + "experimentName": "cctest_archive", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-archive-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-archive/workspaces/cctest-mlws-archive", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/computes/cctestcpuarchive", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-archive/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-archive/environments/cctest-archive-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-archive-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-11T17:58:16.4423309+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" } } ] @@ -73,4 +3157,4 @@ } } ] -} +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-job.json b/tools/c7n_azure/tests_azure/templates/machine-learning-job.json index c5b8fe139e7..94960d3cef7 100644 --- a/tools/c7n_azure/tests_azure/templates/machine-learning-job.json +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-job.json @@ -10,7 +10,7 @@ "storageAccountName": "[concat('cctestmlws', parameters('uniqueId'))]", "keyVaultName": "[concat('cctestmlws', parameters('uniqueId'))]", "appInsightsName": "[concat('cctestmlws', parameters('uniqueId'))]", - "workspaceName": "cctest-mlws", + "workspaceName": "[concat('cctest-mlws-', parameters('uniqueId'))]", "jobName": "cctest-sweep-job" }, "resources": [ @@ -72,19 +72,49 @@ "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" ] }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/cctest-sweep-environment/1', variables('workspaceName'))]", + "properties": { + "environmentType": "UserCreated", + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04", + "isArchived": false, + "osType": "Linux" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, { "type": "Microsoft.MachineLearningServices/workspaces/jobs", "apiVersion": "2023-04-01", "name": "[format('{0}/{1}', variables('workspaceName'), variables('jobName'))]", "properties": { + "isArchived": false, "jobType": "Sweep", "limits": { "jobLimitsType": "Sweep", - "maxConcurrentTrials": 20 + "maxConcurrentTrials": 20, + "maxTotalTrials": 1 + }, + "objective": { + "goal": "Minimize", + "primaryMetric": "loss" + }, + "samplingAlgorithm": { + "samplingAlgorithmType": "Random" + }, + "searchSpace": { + "x": "choice(1)" + }, + "trial": { + "command": "echo loss: 1", + "environmentId": "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), 'cctest-sweep-environment', '1')]" } }, "dependsOn": [ - "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), 'cctest-sweep-environment', '1')]" ] } ] diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py index 8786bdcc3a8..4683d23341c 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py @@ -14,6 +14,21 @@ class MachineLearningJobTest(BaseTest): + def test_machine_learning_job_query_args(self): + parent = { + 'name': 'workspace', + 'resourceGroup': 'resource-group', + } + + self.assertEqual( + { + 'resource_group_name': 'resource-group', + 'workspace_name': 'workspace', + 'list_view_type': 'All', + }, + MachineLearningJob.resource_type.extra_args(parent), + ) + def test_machine_learning_job_schema_validate(self): p = self.load_policy({ 'name': 'find-all-machine-learning-jobs', @@ -37,13 +52,46 @@ def test_machine_learning_job_schema_validate(self): @arm_template('machine-learning-job.json') @cassette_name('machine-learning-jobs') def test_machine_learning_job_query(self): - p = self.load_policy({ - 'name': 'find-all-machine-learning-jobs', + policy = { + 'name': 'find-machine-learning-job', 'resource': 'azure.machine-learning-job', - }) - resources = p.run() + 'filters': [{ + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-job', + }, { + 'type': 'value', + 'key': 'name', + 'value': 'cctest-sweep-job', + }], + } + resources = self.load_policy(policy).run() + self.assertEqual(1, len(resources)) + self.assertFalse(resources[0]['properties']['isArchived']) + + resource = resources[0] + resource_group = ResourceIdParser.get_resource_group(resource['id']) + workspace = ResourceIdParser.get_resource_name(resource['c7n:parent-id']) + client = local_session(Session).client( + 'azure.mgmt.machinelearningservices.MachineLearningServicesMgmtClient') + job = client.jobs.get(resource_group, workspace, resource['name']) + while job.properties.status not in ('Completed', 'Failed', 'Canceled'): + self.sleep_in_live_mode(30) + job = client.jobs.get(resource_group, workspace, resource['name']) + + resource = _serialize(job) + resource['properties']['isArchived'] = True + client.jobs.create_or_update( + resource_group_name=resource_group, + workspace_name=workspace, + id=resource['name'], + body=resource, + ) + + resources = self.load_policy(policy).run() self.assertEqual(1, len(resources)) self.assertEqual('cctest-sweep-job', resources[0]['name']) + self.assertTrue(resources[0]['properties']['isArchived']) self.assertIn('/jobs/', resources[0]['id']) @arm_template('machine-learning-job.json') From a1530aa160dc962076c10d8a725f20494a0ea652 Mon Sep 17 00:00:00 2001 From: Mrinal P R <94679425+mrinalpravi@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:10:13 +0530 Subject: [PATCH 20/65] aws - budget - surface notifications and filter expression (#11116) Co-authored-by: Claude Opus 5 (1M context) --- c7n/resources/budgets.py | 28 ++- ...scribeBudgetNotificationsForAccount_1.json | 27 +++ .../budgets.DescribeBudgets_1.json | 172 ++++++++++++++++++ ...scribeBudgetNotificationsForAccount_1.json | 20 ++ .../budgets.DescribeBudgets_1.json | 11 ++ tests/test_budgets.py | 77 +++++++- 6 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 tests/data/placebo/test_budget_notifications/budgets.DescribeBudgetNotificationsForAccount_1.json create mode 100644 tests/data/placebo/test_budget_notifications/budgets.DescribeBudgets_1.json create mode 100644 tests/data/placebo/test_budget_query/budgets.DescribeBudgetNotificationsForAccount_1.json diff --git a/c7n/resources/budgets.py b/c7n/resources/budgets.py index d8945830992..11a2b1aa68f 100644 --- a/c7n/resources/budgets.py +++ b/c7n/resources/budgets.py @@ -2,12 +2,36 @@ # SPDX-License-Identifier: Apache-2.0 from c7n.manager import resources from c7n import query +from c7n.utils import local_session class DescribeBudget(query.DescribeSource): + def resources(self, query): - required = {"AccountId": self.manager.config.account_id} - return super().resources(required) + params = { + "AccountId": self.manager.config.account_id, + # FilterExpression is omitted from the response unless asked for, + # and supersedes the deprecated CostFilters. + "ShowFilterExpression": True, + } + params.update(query or {}) + return super().resources(params) + + def augment(self, resources): + resources = super().augment(resources) + if not resources: + return resources + client = local_session(self.manager.session_factory).client("budgets") + paginator = client.get_paginator("describe_budget_notifications_for_account") + paginator.PAGE_ITERATOR_CLS = query.RetryPageIterator + notifications = {} + for page in paginator.paginate(AccountId=self.manager.config.account_id): + for b in page.get("BudgetNotificationsForAccount", ()): + notifications.setdefault(b["BudgetName"], []).extend( + b.get("Notifications", ())) + for r in resources: + r["Notifications"] = notifications.get(r["BudgetName"], []) + return resources @resources.register("budget") diff --git a/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgetNotificationsForAccount_1.json b/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgetNotificationsForAccount_1.json new file mode 100644 index 00000000000..bbc2e01f176 --- /dev/null +++ b/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgetNotificationsForAccount_1.json @@ -0,0 +1,27 @@ +{ + "status_code": 200, + "data": { + "BudgetNotificationsForAccount": [ + { + "BudgetName": "budget-ec2-alerted", + "Notifications": [ + { + "NotificationType": "ACTUAL", + "ComparisonOperator": "GREATER_THAN", + "Threshold": 100.0, + "ThresholdType": "PERCENTAGE", + "NotificationState": "ALARM" + }, + { + "NotificationType": "FORECASTED", + "ComparisonOperator": "GREATER_THAN", + "Threshold": 80.0, + "ThresholdType": "PERCENTAGE", + "NotificationState": "OK" + } + ] + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgets_1.json b/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgets_1.json new file mode 100644 index 00000000000..ba8b76a3057 --- /dev/null +++ b/tests/data/placebo/test_budget_notifications/budgets.DescribeBudgets_1.json @@ -0,0 +1,172 @@ +{ + "status_code": 200, + "data": { + "Budgets": [ + { + "BudgetName": "budget-ec2-alerted", + "BudgetLimit": { + "Amount": "1200.0", + "Unit": "USD" + }, + "CostTypes": { + "IncludeTax": true, + "IncludeSubscription": true, + "UseBlended": false, + "IncludeRefund": true, + "IncludeCredit": true, + "IncludeUpfront": true, + "IncludeRecurring": true, + "IncludeOtherSubscription": true, + "IncludeSupport": true, + "IncludeDiscount": true, + "UseAmortized": false + }, + "TimeUnit": "MONTHLY", + "TimePeriod": { + "Start": { + "__class__": "datetime", + "year": 2025, + "month": 1, + "day": 1, + "hour": 0, + "minute": 0, + "second": 0, + "microsecond": 0 + }, + "End": { + "__class__": "datetime", + "year": 2087, + "month": 6, + "day": 15, + "hour": 0, + "minute": 0, + "second": 0, + "microsecond": 0 + } + }, + "CalculatedSpend": { + "ActualSpend": { + "Amount": "55.096", + "Unit": "USD" + }, + "ForecastedSpend": { + "Amount": "242.421", + "Unit": "USD" + } + }, + "BudgetType": "COST", + "AutoAdjustData": { + "AutoAdjustType": "HISTORICAL", + "HistoricalOptions": { + "BudgetAdjustmentPeriod": 6, + "LookBackAvailablePeriods": 6 + } + }, + "FilterExpression": { + "Dimensions": { + "Key": "SERVICE", + "Values": [ + "Amazon Elastic Compute Cloud - Compute" + ], + "MatchOptions": [ + "EQUALS" + ] + } + }, + "LastUpdatedTime": { + "__class__": "datetime", + "year": 2025, + "month": 8, + "day": 8, + "hour": 5, + "minute": 56, + "second": 25, + "microsecond": 388000 + } + }, + { + "BudgetName": "budget-s3-unalerted", + "BudgetLimit": { + "Amount": "300.0", + "Unit": "USD" + }, + "CostTypes": { + "IncludeTax": true, + "IncludeSubscription": true, + "UseBlended": false, + "IncludeRefund": true, + "IncludeCredit": true, + "IncludeUpfront": true, + "IncludeRecurring": true, + "IncludeOtherSubscription": true, + "IncludeSupport": true, + "IncludeDiscount": true, + "UseAmortized": false + }, + "TimeUnit": "MONTHLY", + "TimePeriod": { + "Start": { + "__class__": "datetime", + "year": 2025, + "month": 1, + "day": 1, + "hour": 0, + "minute": 0, + "second": 0, + "microsecond": 0 + }, + "End": { + "__class__": "datetime", + "year": 2087, + "month": 6, + "day": 15, + "hour": 0, + "minute": 0, + "second": 0, + "microsecond": 0 + } + }, + "CalculatedSpend": { + "ActualSpend": { + "Amount": "12.5", + "Unit": "USD" + }, + "ForecastedSpend": { + "Amount": "40.0", + "Unit": "USD" + } + }, + "BudgetType": "COST", + "AutoAdjustData": { + "AutoAdjustType": "HISTORICAL", + "HistoricalOptions": { + "BudgetAdjustmentPeriod": 6, + "LookBackAvailablePeriods": 6 + } + }, + "FilterExpression": { + "Dimensions": { + "Key": "SERVICE", + "Values": [ + "Amazon Simple Storage Service" + ], + "MatchOptions": [ + "EQUALS" + ] + } + }, + "LastUpdatedTime": { + "__class__": "datetime", + "year": 2025, + "month": 8, + "day": 8, + "hour": 6, + "minute": 12, + "second": 3, + "microsecond": 114000 + } + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_budget_query/budgets.DescribeBudgetNotificationsForAccount_1.json b/tests/data/placebo/test_budget_query/budgets.DescribeBudgetNotificationsForAccount_1.json new file mode 100644 index 00000000000..c4dcfc793b4 --- /dev/null +++ b/tests/data/placebo/test_budget_query/budgets.DescribeBudgetNotificationsForAccount_1.json @@ -0,0 +1,20 @@ +{ + "status_code": 200, + "data": { + "BudgetNotificationsForAccount": [ + { + "BudgetName": "budget-ec2-monthly", + "Notifications": [ + { + "NotificationType": "FORECASTED", + "ComparisonOperator": "GREATER_THAN", + "Threshold": 100.0, + "ThresholdType": "PERCENTAGE", + "NotificationState": "OK" + } + ] + } + ], + "ResponseMetadata": {} + } +} diff --git a/tests/data/placebo/test_budget_query/budgets.DescribeBudgets_1.json b/tests/data/placebo/test_budget_query/budgets.DescribeBudgets_1.json index f627c342559..f82c27eb380 100644 --- a/tests/data/placebo/test_budget_query/budgets.DescribeBudgets_1.json +++ b/tests/data/placebo/test_budget_query/budgets.DescribeBudgets_1.json @@ -60,6 +60,17 @@ } }, "BudgetType": "COST", + "FilterExpression": { + "Dimensions": { + "Key": "SERVICE", + "Values": [ + "Amazon Elastic Compute Cloud - Compute" + ], + "MatchOptions": [ + "EQUALS" + ] + } + }, "LastUpdatedTime": { "__class__": "datetime", "year": 2023, diff --git a/tests/test_budgets.py b/tests/test_budgets.py index fab9c2edd7f..3dfb3dd1ccf 100644 --- a/tests/test_budgets.py +++ b/tests/test_budgets.py @@ -1,6 +1,8 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from unittest.mock import patch + from pytest_terraform import terraform from .zpill import ACCOUNT_ID @@ -16,4 +18,77 @@ def test_budget_query(test, budgets_budget): }, session_factory=factory, config={"account_id": ACCOUNT_ID}) resources = policy.run() - assert len(resources) > 0 + assert len(resources) == 1 + assert resources[0]["Notifications"] == [{ + "NotificationType": "FORECASTED", + "ComparisonOperator": "GREATER_THAN", + "Threshold": 100.0, + "ThresholdType": "PERCENTAGE", + "NotificationState": "OK", + }] + assert resources[0]["FilterExpression"]["Dimensions"]["Key"] == "SERVICE" + + +def test_budget_notifications(test): + factory = test.replay_flight_data("test_budget_notifications") + + policy = test.load_policy({ + "name": "aws-budget-notifications", + "resource": "aws.budget", + }, session_factory=factory, config={"account_id": ACCOUNT_ID}) + + notifications = { + r["BudgetName"]: r["Notifications"] for r in policy.run()} + assert len(notifications["budget-ec2-alerted"]) == 2 + # a budget without notifications gets an empty collection, not a missing key + assert notifications["budget-s3-unalerted"] == [] + + +def test_budget_missing_actual_spend_alert(test): + factory = test.replay_flight_data("test_budget_notifications") + + policy = test.load_policy({ + "name": "aws-budget-missing-actual-spend-alert", + "resource": "aws.budget", + "filters": [ + {"type": "value", "key": "BudgetType", "value": "COST"}, + {"type": "value", "key": "FilterExpression.Dimensions.Key", + "value": "SERVICE"}, + {"or": [ + {"type": "value", "key": "AutoAdjustData.AutoAdjustType", + "op": "ne", "value": "HISTORICAL"}, + {"type": "value", + "key": "Notifications[?NotificationType=='ACTUAL'" + " && ComparisonOperator=='GREATER_THAN']", + "value": "empty"}, + ]}, + ], + }, session_factory=factory, config={"account_id": ACCOUNT_ID}) + + resources = policy.run() + assert [r["BudgetName"] for r in resources] == ["budget-s3-unalerted"] + + +def test_budget_query_params_merged(test): + factory = test.replay_flight_data("test_budget_notifications") + + policy = test.load_policy({ + "name": "aws-budget-enum", + "resource": "aws.budget", + }, session_factory=factory, config={"account_id": ACCOUNT_ID}) + + source = policy.resource_manager.source + captured = {} + + def record(client, enum_op, params, path, retry=None): + captured.update(params) + return [] + + with patch.object(source.query, "_invoke_client_enum", record): + source.resources({"MaxResults": 50}) + + assert captured == { + "AccountId": ACCOUNT_ID, + "ShowFilterExpression": True, + "MaxResults": 50, + } From f8bb1d320938362e1d73472c3cc2ef4a9a757f2e Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Tue, 29 Sep 2026 23:16:22 -0400 Subject: [PATCH 21/65] azure - network-security-group - add remove-rules action (#11137) --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../resources/network_security_group.py | 54 ++ tools/c7n_azure/tests_azure/azure_common.py | 3 + ...ve_rules_removes_matched_ingress_rule.json | 578 ++++++++++++++++++ .../networksecuritygroup-remove-rules.json | 36 ++ .../test_networksecuritygroup.py | 119 +++- 5 files changed, 788 insertions(+), 2 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/NetworkSecurityGroupTest.test_remove_rules_removes_matched_ingress_rule.json create mode 100644 tools/c7n_azure/tests_azure/templates/networksecuritygroup-remove-rules.json diff --git a/tools/c7n_azure/c7n_azure/resources/network_security_group.py b/tools/c7n_azure/c7n_azure/resources/network_security_group.py index 8ad2085d65a..6a7fd172c55 100644 --- a/tools/c7n_azure/c7n_azure/resources/network_security_group.py +++ b/tools/c7n_azure/c7n_azure/resources/network_security_group.py @@ -10,6 +10,7 @@ from c7n.filters import Filter, FilterValidationError from c7n.filters.core import PolicyValidationError, ValueFilter from c7n.utils import type_schema +from c7n_azure.actions.base import AzureBaseAction from c7n_azure.provider import resources from c7n_azure.resources.arm import ArmResourceManager from c7n_azure.utils import PortsRangeHelper, StringUtils @@ -459,3 +460,56 @@ class OpenRules(NetworkSecurityGroupPortsAction): """ schema = type_schema('open', rinherit=NetworkSecurityGroupPortsAction.schema) access_action = ALLOW_OPERATION + + +@NetworkSecurityGroup.action_registry.register('remove-rules') +class RemoveRules(AzureBaseAction): + """Remove security rules from a Network Security Group + + Deletes the rules matched by an ``ingress`` or ``egress`` filter, or every rule in + that direction with ``all``. Unlike ``close``, which adds a higher priority Deny + rule above the offending one, this removes the offending rule itself. + + :example: + + Remove inbound rules that allow traffic from any source + + .. code-block:: yaml + + policies: + - name: remove-public-ingress + resource: azure.networksecuritygroup + filters: + - type: ingress + source: '*' + access: Allow + actions: + - type: remove-rules + ingress: matched + """ + schema = type_schema( + 'remove-rules', + ingress={'type': 'string', 'enum': ['matched', 'all']}, + egress={'type': 'string', 'enum': ['matched', 'all']}, + ) + + def _prepare_processing(self): + self.client = self.manager.get_client() + + def _process_resource(self, nsg): + for key, direction, annotation in ( + ('ingress', 'Inbound', IngressFilter.matched_annotation_key), + ('egress', 'Outbound', EgressFilter.matched_annotation_key)): + + mode = self.data.get(key, 'matched') + if mode == 'matched': + rules = nsg.get(annotation, ()) + elif mode == 'all': + rules = [r for r in nsg['properties']['securityRules'] + if StringUtils.equal(r['properties']['direction'], direction)] + else: + continue + + for rule in rules: + self.client.security_rules.begin_delete( + nsg['resourceGroup'], nsg['name'], rule['name']).result() diff --git a/tools/c7n_azure/tests_azure/azure_common.py b/tools/c7n_azure/tests_azure/azure_common.py index 2db6bdff189..a195cb0de3d 100644 --- a/tools/c7n_azure/tests_azure/azure_common.py +++ b/tools/c7n_azure/tests_azure/azure_common.py @@ -256,6 +256,9 @@ def _response_callback(self, response): response['headers'].items() if k.lower() not in self.FILTERED_HEADERS} + if not response['body'].get('string'): + return response + content_type = response['headers'].get('content-type', (None,))[0] if not content_type or 'application/json' not in content_type: return response diff --git a/tools/c7n_azure/tests_azure/cassettes/NetworkSecurityGroupTest.test_remove_rules_removes_matched_ingress_rule.json b/tools/c7n_azure/tests_azure/cassettes/NetworkSecurityGroupTest.test_remove_rules_removes_matched_ingress_rule.json new file mode 100644 index 00000000000..78ddc8ff2f7 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/NetworkSecurityGroupTest.test_remove_rules_removes_matched_ingress_rule.json @@ -0,0 +1,578 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/networkSecurityGroups?api-version=2024-05-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-throttle-levels": [ + "operationConcurrencyPct=0.5, subscriptionReadRatePct=0.2, etc" + ], + "x-ms-original-request-ids": [ + "2e07a321-a603-46b9-9d1b-e1a9272a34e0" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-arm-service-request-id": [ + "786eca44-f386-4add-80c2-3fb207691add" + ], + "content-length": [ + "5862" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: 86290B88D35F4B4097035C9D4483F825 Ref B: CH1AA2020620035 Ref C: 2026-09-28T20:26:34Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Mon, 28 Sep 2026 20:26:34 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "c7n-nsg-remove-rules", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups", + "location": "southcentralus", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "7a749cea-eac1-420b-9cc9-66b20cd394c5", + "securityRules": [ + { + "name": "allow-all-inbound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/securityRules/allow-all-inbound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "properties": { + "provisioningState": "Succeeded", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 100, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + } + ], + "defaultSecurityRules": [ + { + "name": "AllowVnetInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowVnetInBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow inbound traffic from all VMs in VNET", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "VirtualNetwork", + "destinationAddressPrefix": "VirtualNetwork", + "access": "Allow", + "priority": 65000, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowAzureLoadBalancerInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowAzureLoadBalancerInBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow inbound traffic from azure load balancer", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "AzureLoadBalancer", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 65001, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "DenyAllInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/DenyAllInBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Deny all inbound traffic", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Deny", + "priority": 65500, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowVnetOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowVnetOutBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow outbound traffic from all VMs to all VMs in VNET", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "VirtualNetwork", + "destinationAddressPrefix": "VirtualNetwork", + "access": "Allow", + "priority": 65000, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowInternetOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowInternetOutBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow outbound traffic from all VMs to Internet", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "Internet", + "access": "Allow", + "priority": 65001, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "DenyAllOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/DenyAllOutBound", + "etag": "W/\"9d4bec51-8412-4d9d-b846-4c5a724292ff\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Deny all outbound traffic", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Deny", + "priority": 65500, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + } + ] + } + } + ] + } + } + } + }, + { + "request": { + "method": "DELETE", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/securityRules/allow-all-inbound?api-version=2024-05-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "Accepted" + }, + "headers": { + "content-length": [ + "0" + ], + "x-msedge-ref": [ + "Ref A: 0CDB434A7CE64774A469896F45425667 Ref B: CHI261140506042 Ref C: 2026-09-28T20:26:35Z" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operationResults/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239954280163&c=MIIHlDCCBnygAwIBAgIQEWqq0LXHm9_RoSQk3rtLtzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEytDQ01FIEcxIFRMUyBSU0EgMjA0OCBTSEEyNTYgMjA0OSBXVVMyIENBIDAxMB4XDTI2MDkyMzA2MDIzOVoXDTI2MTIxODEyMDIzOVowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTL3s1G4HRJxCxQWgaa1QhZF9rCHBTzLl8phyc14DtHqFD1UEEvU8TdKIkn63TfPvm-CD5nDTQJhmqP3TQ4a1zbeIZvN26UwO-f2zs8v284_6vqmQYpp074W-XCE3yUu6xZi0c2RjrWXRE0C83U9RwiZBkm4sAg6EaNrBTlMtlbtByJMhA9KOAfS_ME_eoDeS2P_o__hD4yg538D4itpA1Yp3-XcF3vu3MgpyPUf4o8rU8J3Y9-jIPfdgYdhE-QPuKdwKQu0Aov3aNNHzv_aJivc4z22sXdAC9MeLP59Q0e1QaF_moRqk0Sr9AyE7J1ohMafagWSkQ-fj_5H83r6J9AgMBAAGjggSSMIIEjjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBTI83RcASQN5gnS06-jdiNGf7b_JDAfBgNVHSMEGDAWgBSs43L6A7Jznj2VyO-HW67dG6HtaDCCAbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czIvY3Jscy9jY21ld2VzdHVzMnBraS9jY21ld2VzdHVzMmljYTAxLzk3L2N1cnJlbnQuY3JsMFqgWKBWhlRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6Ly9jY21ld2VzdHVzMnBraS53ZXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZXdlc3R1czJpY2EwMS85Ny9jdXJyZW50LmNybDCCAbcGCCsGAQUFBwEBBIIBqTCCAaUwbAYIKwYBBQUHMAKGYGh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBuBggrBgEFBQcwAoZiaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jYWNlcnRzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvY2VydC5jZXIwXQYIKwYBBQUHMAKGUWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBmBggrBgEFBQcwAoZaaHR0cDovL2NjbWV3ZXN0dXMycGtpLndlc3R1czIucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRob3JpdGllcy9jY21ld2VzdHVzMmljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBtlTS-mdvGIKziCDp4X9OVPqRLuoH48ib0L8ZVUxxNXw-O3sl-lio6Y7V0Xqv47B1Ru94KfFJZpXsF6u0Gk7IcfpKl2PltvEAmAKSdpsOElmGW0MeH-AsmLU-uRKqaIGsDrnsyJuLqtILSJ_GCMnG1VbnaqOJFmoBMj7f_unbCkVhb6MmyJC4sd_klisVbuUnjtp_Cgp2Wc_HeRKPemyn8alnsmHVOO0NHWIR41o6dNQCt7LJ5nM5rQd2Fww6uP0eqn3jQiNE0nl_zOqY8Me96xb0gWGe9D4aRXyAG_j-B4_gTga7SVfy92csbUhBWi9huq3gKSoRRpuS_4dQqvDVj&s=gOGnZMZu_2DMKE27tRTfgUIgMGCsgccDFOIJi2YUaE1O1pLpGly4S1JdiubxcY68dCDej-WTV3OpM3MCfbl13DYt51zOa6QcS0IKk-7SAQCMhBSqiW5KbUrV7ym2cdWTHMS4L2sG9J7tt4ssITnSr__feBDP9WEePF2AyPF8QcuF4kXwXkSxbtKT867YUtQqAal_vWgZCswKv9UbW7vbic59meYn5J9r92LdTqsk67_cSXGs6rRk61-OzyROMUxMlt65QLJwMjlgThyFO0F1yKM1VYQ6szbF4NY1hEVlWb2e_EnFJKBIDTVgJ4N29t6TS7Gq8P8B3Tn86sc7GbB15A&h=wWaJRu2WikkPOzX9OZ973fqFt3Sacq2pKYdENISV98E" + ], + "x-ms-throttle-levels": [ + "operationRatePct=0.1, operationConcurrencyPct=0.3, subscriptionWriteRatePct=0.4, etc" + ], + "date": [ + "Mon, 28 Sep 2026 20:26:35 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-arm-service-request-id": [ + "f8d30418-d7ba-4f10-a8bd-3cdedbc49c76" + ], + "x-ms-ratelimit-remaining-subscription-global-deletes": [ + "2999" + ], + "x-ms-ratelimit-remaining-subscription-deletes": [ + "199" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operations/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239954280163&c=MIIHlDCCBnygAwIBAgIQEWqq0LXHm9_RoSQk3rtLtzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEytDQ01FIEcxIFRMUyBSU0EgMjA0OCBTSEEyNTYgMjA0OSBXVVMyIENBIDAxMB4XDTI2MDkyMzA2MDIzOVoXDTI2MTIxODEyMDIzOVowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTL3s1G4HRJxCxQWgaa1QhZF9rCHBTzLl8phyc14DtHqFD1UEEvU8TdKIkn63TfPvm-CD5nDTQJhmqP3TQ4a1zbeIZvN26UwO-f2zs8v284_6vqmQYpp074W-XCE3yUu6xZi0c2RjrWXRE0C83U9RwiZBkm4sAg6EaNrBTlMtlbtByJMhA9KOAfS_ME_eoDeS2P_o__hD4yg538D4itpA1Yp3-XcF3vu3MgpyPUf4o8rU8J3Y9-jIPfdgYdhE-QPuKdwKQu0Aov3aNNHzv_aJivc4z22sXdAC9MeLP59Q0e1QaF_moRqk0Sr9AyE7J1ohMafagWSkQ-fj_5H83r6J9AgMBAAGjggSSMIIEjjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBTI83RcASQN5gnS06-jdiNGf7b_JDAfBgNVHSMEGDAWgBSs43L6A7Jznj2VyO-HW67dG6HtaDCCAbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czIvY3Jscy9jY21ld2VzdHVzMnBraS9jY21ld2VzdHVzMmljYTAxLzk3L2N1cnJlbnQuY3JsMFqgWKBWhlRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6Ly9jY21ld2VzdHVzMnBraS53ZXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZXdlc3R1czJpY2EwMS85Ny9jdXJyZW50LmNybDCCAbcGCCsGAQUFBwEBBIIBqTCCAaUwbAYIKwYBBQUHMAKGYGh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBuBggrBgEFBQcwAoZiaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jYWNlcnRzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvY2VydC5jZXIwXQYIKwYBBQUHMAKGUWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBmBggrBgEFBQcwAoZaaHR0cDovL2NjbWV3ZXN0dXMycGtpLndlc3R1czIucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRob3JpdGllcy9jY21ld2VzdHVzMmljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBtlTS-mdvGIKziCDp4X9OVPqRLuoH48ib0L8ZVUxxNXw-O3sl-lio6Y7V0Xqv47B1Ru94KfFJZpXsF6u0Gk7IcfpKl2PltvEAmAKSdpsOElmGW0MeH-AsmLU-uRKqaIGsDrnsyJuLqtILSJ_GCMnG1VbnaqOJFmoBMj7f_unbCkVhb6MmyJC4sd_klisVbuUnjtp_Cgp2Wc_HeRKPemyn8alnsmHVOO0NHWIR41o6dNQCt7LJ5nM5rQd2Fww6uP0eqn3jQiNE0nl_zOqY8Me96xb0gWGe9D4aRXyAG_j-B4_gTga7SVfy92csbUhBWi9huq3gKSoRRpuS_4dQqvDVj&s=IIM5yZ_dqv6pUZWUNuCMYBXRMkq1JZTuTLDxl8GnEkd-RiRyEQzQBEDun8WZxg3lydJPvufsjGIe3SvdgeODJYB_JmgqSlrtef8ZZzf1z6mZDbl4BeDrH2xSMDdDUtQ0Gs2Su1_dOA-GFLtWfEKpaB_E2y3SNH3s7CyasirJaYSFf_4SXbA_FUNIJZCx0kdxf8WH7DzTG7QXYreMwSpWBFIYG5zuVF7ulEt_5_CeAsy-Nx9iU5wTbWKvJszX5t5UrUcn4xT5k2tuGLY8iw4t8BtUiBaxvf3zuCUI96GHFl78_qqiwY1RQYkrBUokdZd-peQGb0gza7BhIYMHdiAXBQ&h=YEzgNXSlESFSNW-Rf64v5VkWsb9D6hd3M96uDb_KNOM" + ] + }, + "body": { + "string": "" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operations/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239954280163&c=MIIHlDCCBnygAwIBAgIQEWqq0LXHm9_RoSQk3rtLtzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEytDQ01FIEcxIFRMUyBSU0EgMjA0OCBTSEEyNTYgMjA0OSBXVVMyIENBIDAxMB4XDTI2MDkyMzA2MDIzOVoXDTI2MTIxODEyMDIzOVowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTL3s1G4HRJxCxQWgaa1QhZF9rCHBTzLl8phyc14DtHqFD1UEEvU8TdKIkn63TfPvm-CD5nDTQJhmqP3TQ4a1zbeIZvN26UwO-f2zs8v284_6vqmQYpp074W-XCE3yUu6xZi0c2RjrWXRE0C83U9RwiZBkm4sAg6EaNrBTlMtlbtByJMhA9KOAfS_ME_eoDeS2P_o__hD4yg538D4itpA1Yp3-XcF3vu3MgpyPUf4o8rU8J3Y9-jIPfdgYdhE-QPuKdwKQu0Aov3aNNHzv_aJivc4z22sXdAC9MeLP59Q0e1QaF_moRqk0Sr9AyE7J1ohMafagWSkQ-fj_5H83r6J9AgMBAAGjggSSMIIEjjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBTI83RcASQN5gnS06-jdiNGf7b_JDAfBgNVHSMEGDAWgBSs43L6A7Jznj2VyO-HW67dG6HtaDCCAbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czIvY3Jscy9jY21ld2VzdHVzMnBraS9jY21ld2VzdHVzMmljYTAxLzk3L2N1cnJlbnQuY3JsMFqgWKBWhlRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6Ly9jY21ld2VzdHVzMnBraS53ZXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZXdlc3R1czJpY2EwMS85Ny9jdXJyZW50LmNybDCCAbcGCCsGAQUFBwEBBIIBqTCCAaUwbAYIKwYBBQUHMAKGYGh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBuBggrBgEFBQcwAoZiaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jYWNlcnRzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvY2VydC5jZXIwXQYIKwYBBQUHMAKGUWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBmBggrBgEFBQcwAoZaaHR0cDovL2NjbWV3ZXN0dXMycGtpLndlc3R1czIucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRob3JpdGllcy9jY21ld2VzdHVzMmljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBtlTS-mdvGIKziCDp4X9OVPqRLuoH48ib0L8ZVUxxNXw-O3sl-lio6Y7V0Xqv47B1Ru94KfFJZpXsF6u0Gk7IcfpKl2PltvEAmAKSdpsOElmGW0MeH-AsmLU-uRKqaIGsDrnsyJuLqtILSJ_GCMnG1VbnaqOJFmoBMj7f_unbCkVhb6MmyJC4sd_klisVbuUnjtp_Cgp2Wc_HeRKPemyn8alnsmHVOO0NHWIR41o6dNQCt7LJ5nM5rQd2Fww6uP0eqn3jQiNE0nl_zOqY8Me96xb0gWGe9D4aRXyAG_j-B4_gTga7SVfy92csbUhBWi9huq3gKSoRRpuS_4dQqvDVj&s=IIM5yZ_dqv6pUZWUNuCMYBXRMkq1JZTuTLDxl8GnEkd-RiRyEQzQBEDun8WZxg3lydJPvufsjGIe3SvdgeODJYB_JmgqSlrtef8ZZzf1z6mZDbl4BeDrH2xSMDdDUtQ0Gs2Su1_dOA-GFLtWfEKpaB_E2y3SNH3s7CyasirJaYSFf_4SXbA_FUNIJZCx0kdxf8WH7DzTG7QXYreMwSpWBFIYG5zuVF7ulEt_5_CeAsy-Nx9iU5wTbWKvJszX5t5UrUcn4xT5k2tuGLY8iw4t8BtUiBaxvf3zuCUI96GHFl78_qqiwY1RQYkrBUokdZd-peQGb0gza7BhIYMHdiAXBQ&h=YEzgNXSlESFSNW-Rf64v5VkWsb9D6hd3M96uDb_KNOM", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-throttle-levels": [ + "operationConcurrencyPct=0.5, subscriptionReadRatePct=0.2, etc" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-arm-service-request-id": [ + "4844ce2c-f7d6-4621-b6f5-e31173ff00ac" + ], + "content-length": [ + "22" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: 82C65C3FDD2D40E9A19B09D94B00C158 Ref B: CHI261140511036 Ref C: 2026-09-28T20:26:35Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Mon, 28 Sep 2026 20:26:35 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "status": "Succeeded" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operationResults/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239954280163&c=MIIHlDCCBnygAwIBAgIQEWqq0LXHm9_RoSQk3rtLtzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEytDQ01FIEcxIFRMUyBSU0EgMjA0OCBTSEEyNTYgMjA0OSBXVVMyIENBIDAxMB4XDTI2MDkyMzA2MDIzOVoXDTI2MTIxODEyMDIzOVowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTL3s1G4HRJxCxQWgaa1QhZF9rCHBTzLl8phyc14DtHqFD1UEEvU8TdKIkn63TfPvm-CD5nDTQJhmqP3TQ4a1zbeIZvN26UwO-f2zs8v284_6vqmQYpp074W-XCE3yUu6xZi0c2RjrWXRE0C83U9RwiZBkm4sAg6EaNrBTlMtlbtByJMhA9KOAfS_ME_eoDeS2P_o__hD4yg538D4itpA1Yp3-XcF3vu3MgpyPUf4o8rU8J3Y9-jIPfdgYdhE-QPuKdwKQu0Aov3aNNHzv_aJivc4z22sXdAC9MeLP59Q0e1QaF_moRqk0Sr9AyE7J1ohMafagWSkQ-fj_5H83r6J9AgMBAAGjggSSMIIEjjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBTI83RcASQN5gnS06-jdiNGf7b_JDAfBgNVHSMEGDAWgBSs43L6A7Jznj2VyO-HW67dG6HtaDCCAbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czIvY3Jscy9jY21ld2VzdHVzMnBraS9jY21ld2VzdHVzMmljYTAxLzk3L2N1cnJlbnQuY3JsMFqgWKBWhlRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6Ly9jY21ld2VzdHVzMnBraS53ZXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZXdlc3R1czJpY2EwMS85Ny9jdXJyZW50LmNybDCCAbcGCCsGAQUFBwEBBIIBqTCCAaUwbAYIKwYBBQUHMAKGYGh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBuBggrBgEFBQcwAoZiaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jYWNlcnRzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvY2VydC5jZXIwXQYIKwYBBQUHMAKGUWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBmBggrBgEFBQcwAoZaaHR0cDovL2NjbWV3ZXN0dXMycGtpLndlc3R1czIucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRob3JpdGllcy9jY21ld2VzdHVzMmljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBtlTS-mdvGIKziCDp4X9OVPqRLuoH48ib0L8ZVUxxNXw-O3sl-lio6Y7V0Xqv47B1Ru94KfFJZpXsF6u0Gk7IcfpKl2PltvEAmAKSdpsOElmGW0MeH-AsmLU-uRKqaIGsDrnsyJuLqtILSJ_GCMnG1VbnaqOJFmoBMj7f_unbCkVhb6MmyJC4sd_klisVbuUnjtp_Cgp2Wc_HeRKPemyn8alnsmHVOO0NHWIR41o6dNQCt7LJ5nM5rQd2Fww6uP0eqn3jQiNE0nl_zOqY8Me96xb0gWGe9D4aRXyAG_j-B4_gTga7SVfy92csbUhBWi9huq3gKSoRRpuS_4dQqvDVj&s=gOGnZMZu_2DMKE27tRTfgUIgMGCsgccDFOIJi2YUaE1O1pLpGly4S1JdiubxcY68dCDej-WTV3OpM3MCfbl13DYt51zOa6QcS0IKk-7SAQCMhBSqiW5KbUrV7ym2cdWTHMS4L2sG9J7tt4ssITnSr__feBDP9WEePF2AyPF8QcuF4kXwXkSxbtKT867YUtQqAal_vWgZCswKv9UbW7vbic59meYn5J9r92LdTqsk67_cSXGs6rRk61-OzyROMUxMlt65QLJwMjlgThyFO0F1yKM1VYQ6szbF4NY1hEVlWb2e_EnFJKBIDTVgJ4N29t6TS7Gq8P8B3Tn86sc7GbB15A&h=wWaJRu2WikkPOzX9OZ973fqFt3Sacq2pKYdENISV98E", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 204, + "message": "No Content" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 5F06D461FF6B4E03BAA2334CACA170ED Ref B: CHI261140509040 Ref C: 2026-09-28T20:26:36Z" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operationResults/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239964451012&c=MIIHxDCCBqygAwIBAgIRAK3GpGj7DjHLrjRJRX_HEXgwDQYJKoZIhvcNAQELBQAwNTEzMDEGA1UEAxMqQ0NNRSBHMSBUTFMgUlNBIDIwNDggU0hBMjU2IDIwNDkgQ1VTIENBIDAxMB4XDTI2MDkxNzE5NDgyOFoXDTI2MTIxMzAxNDgyOFowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1snhTC6g_4xx4ZrHyiUqTYGCsPZny3WTFt1HNTpetNpb_Y4Ect_LxssSA3PaPbNW5xiK2g0l84t8tlBb4NJt4iWD9ZwS9IRxGg0UuWjhyi4BQ5nI7V4c80TQ3W166AA9VV-hnq8DlFZxA4jhgbhfitgCsrhHu-0To9jdy2yVo7Ou3R57j51u2Des7d-kBfWmS0Bfd_rg2U-JsWvitd4mXfjVmh0hIeD8PJAVxRczpzyxghRX3mYtuvxUHM0nYb39mkKTBJyFzkEdLMQXLeDBqfD7O80rXsQ0zETCpv5pYUNhLvtOvWmOOIdBJbNx5pzxskbmMOFz9HgF_Ze6P-F4tAgMBAAGjggTCMIIEvjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBQa8-eemI0KngUPnocA3BYB2hcHRTAfBgNVHSMEGDAWgBT85FoKL4UO50S5B3N44NREB6IZETCCAcoGA1UdHwSCAcEwggG9MG-gbaBrhmlodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvY2VudHJhbHVzL2NybHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvNjYvY3VycmVudC5jcmwwcaBvoG2Ga2h0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L2NlbnRyYWx1cy9jcmxzL2NjbWVjZW50cmFsdXNwa2kvY2NtZWNlbnRyYWx1c2ljYTAxLzY2L2N1cnJlbnQuY3JsMGCgXqBchlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vY2VudHJhbHVzL2NybHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvNjYvY3VycmVudC5jcmwwdaBzoHGGb2h0dHA6Ly9jY21lY2VudHJhbHVzcGtpLmNlbnRyYWx1cy5wa2kuY29yZS53aW5kb3dzLm5ldC9jZXJ0aWZpY2F0ZUF1dGhvcml0aWVzL2NjbWVjZW50cmFsdXNpY2EwMS82Ni9jdXJyZW50LmNybDCCAc8GCCsGAQUFBwEBBIIBwTCCAb0wcgYIKwYBBQUHMAKGZmh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC9jZW50cmFsdXMvY2FjZXJ0cy9jY21lY2VudHJhbHVzcGtpL2NjbWVjZW50cmFsdXNpY2EwMS9jZXJ0LmNlcjB0BggrBgEFBQcwAoZoaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvY2VudHJhbHVzL2NhY2VydHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvY2VydC5jZXIwYwYIKwYBBQUHMAKGV2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9jZW50cmFsdXMvY2FjZXJ0cy9jY21lY2VudHJhbHVzcGtpL2NjbWVjZW50cmFsdXNpY2EwMS9jZXJ0LmNlcjBsBggrBgEFBQcwAoZgaHR0cDovL2NjbWVjZW50cmFsdXNwa2kuY2VudHJhbHVzLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZWNlbnRyYWx1c2ljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBp0qSFIPxsO9-AAkHEku0ZFsMQQDXzDwru9rO_zch628Re4yuZQiurXQmd_7RdfMOTB8Bpy0TLqDaFsVzhDV6VVXcq5tbuMwjxgwFhwDMBRKGkvtXfojeQ5a2wNFt-Pk-_FxKYvNZ4EVdNbJk6d4ify7yuugiGxToKqqxmwp_mgkz71K-9wTj2SvITSyg43qHdnc5fhGdG0VkvOsjuQzgNR02mqPSkLzkVQNwM71gx5P_WA0n3PfHnBKalXfb2chNsstaOtnYkkJcYcNESFK-srFA6sijtKWm_7sbIQcnAJYR5l-eaXxcu2xMuuskUFmgq6njXPHx5tPSNB77zeeFd&s=a9cBDthpadtcSzQoW6tc3u9QOspl8bE1-SOwwSCo1gVofZHt7P_jfpI6OrU96u7X9wGd8_1r95NmG0YawDlRDjvPS9NMDYbX4uITVVPZ6ALMnq0PVBDIOpaba1fVT5GOiOp40dd9LEx_ClkU4RduZRTJqKwIF7pqzvrKtvpRKD5wfbl-FwbA_1c_Vg-C-I6Qc7YUek1-06WJMuB-f1LDD0s9VVfiqm3HVel7v19gwfUpjbVZtidAx6N6p5oYqzQ625b48gL89Sp6aRcBQqhEmLChyV0ZJrdaYtbckxi8rKimmYzfYXdbr1FOqK9689zXHqc8lwWKVgCJEUv3FxgeLg&h=VWLQXyhHVmG9XXiy4T_UzSkDxh3IRTsngwQ7xCjYX5Y" + ], + "x-ms-throttle-levels": [ + "operationConcurrencyPct=0.5, subscriptionReadRatePct=0.2, etc" + ], + "date": [ + "Mon, 28 Sep 2026 20:26:36 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-arm-service-request-id": [ + "f8d30418-d7ba-4f10-a8bd-3cdedbc49c76" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/locations/southcentralus/operations/2e31ee44-4ca9-4b74-8025-ac68c1e03891?api-version=2024-05-01&t=639262239964294856&c=MIIHxDCCBqygAwIBAgIRAK3GpGj7DjHLrjRJRX_HEXgwDQYJKoZIhvcNAQELBQAwNTEzMDEGA1UEAxMqQ0NNRSBHMSBUTFMgUlNBIDIwNDggU0hBMjU2IDIwNDkgQ1VTIENBIDAxMB4XDTI2MDkxNzE5NDgyOFoXDTI2MTIxMzAxNDgyOFowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1snhTC6g_4xx4ZrHyiUqTYGCsPZny3WTFt1HNTpetNpb_Y4Ect_LxssSA3PaPbNW5xiK2g0l84t8tlBb4NJt4iWD9ZwS9IRxGg0UuWjhyi4BQ5nI7V4c80TQ3W166AA9VV-hnq8DlFZxA4jhgbhfitgCsrhHu-0To9jdy2yVo7Ou3R57j51u2Des7d-kBfWmS0Bfd_rg2U-JsWvitd4mXfjVmh0hIeD8PJAVxRczpzyxghRX3mYtuvxUHM0nYb39mkKTBJyFzkEdLMQXLeDBqfD7O80rXsQ0zETCpv5pYUNhLvtOvWmOOIdBJbNx5pzxskbmMOFz9HgF_Ze6P-F4tAgMBAAGjggTCMIIEvjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBQa8-eemI0KngUPnocA3BYB2hcHRTAfBgNVHSMEGDAWgBT85FoKL4UO50S5B3N44NREB6IZETCCAcoGA1UdHwSCAcEwggG9MG-gbaBrhmlodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvY2VudHJhbHVzL2NybHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvNjYvY3VycmVudC5jcmwwcaBvoG2Ga2h0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L2NlbnRyYWx1cy9jcmxzL2NjbWVjZW50cmFsdXNwa2kvY2NtZWNlbnRyYWx1c2ljYTAxLzY2L2N1cnJlbnQuY3JsMGCgXqBchlpodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vY2VudHJhbHVzL2NybHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvNjYvY3VycmVudC5jcmwwdaBzoHGGb2h0dHA6Ly9jY21lY2VudHJhbHVzcGtpLmNlbnRyYWx1cy5wa2kuY29yZS53aW5kb3dzLm5ldC9jZXJ0aWZpY2F0ZUF1dGhvcml0aWVzL2NjbWVjZW50cmFsdXNpY2EwMS82Ni9jdXJyZW50LmNybDCCAc8GCCsGAQUFBwEBBIIBwTCCAb0wcgYIKwYBBQUHMAKGZmh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC9jZW50cmFsdXMvY2FjZXJ0cy9jY21lY2VudHJhbHVzcGtpL2NjbWVjZW50cmFsdXNpY2EwMS9jZXJ0LmNlcjB0BggrBgEFBQcwAoZoaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvY2VudHJhbHVzL2NhY2VydHMvY2NtZWNlbnRyYWx1c3BraS9jY21lY2VudHJhbHVzaWNhMDEvY2VydC5jZXIwYwYIKwYBBQUHMAKGV2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9jZW50cmFsdXMvY2FjZXJ0cy9jY21lY2VudHJhbHVzcGtpL2NjbWVjZW50cmFsdXNpY2EwMS9jZXJ0LmNlcjBsBggrBgEFBQcwAoZgaHR0cDovL2NjbWVjZW50cmFsdXNwa2kuY2VudHJhbHVzLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZWNlbnRyYWx1c2ljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBp0qSFIPxsO9-AAkHEku0ZFsMQQDXzDwru9rO_zch628Re4yuZQiurXQmd_7RdfMOTB8Bpy0TLqDaFsVzhDV6VVXcq5tbuMwjxgwFhwDMBRKGkvtXfojeQ5a2wNFt-Pk-_FxKYvNZ4EVdNbJk6d4ify7yuugiGxToKqqxmwp_mgkz71K-9wTj2SvITSyg43qHdnc5fhGdG0VkvOsjuQzgNR02mqPSkLzkVQNwM71gx5P_WA0n3PfHnBKalXfb2chNsstaOtnYkkJcYcNESFK-srFA6sijtKWm_7sbIQcnAJYR5l-eaXxcu2xMuuskUFmgq6njXPHx5tPSNB77zeeFd&s=fax8iu0CpbursaC7BpMQ6OsjYQMiTmPrSTEkH-myM8fnzkX0b0Ra5ySDA4Ki-dl7D7SHg6GLHvFypmgEglnBkbVjE-EpALskU6uZjYvls2o34Wc1e9FwTVruCL1ySLL0fS-mpVA0dA0sQ4CFg5arMnnr81zpVK-SlXnbvk4h8u8YC2_AM40rqj9LuNyBfTmZxWL2c6pnGAZzQllejT9wCwvtEods3RWSSYOWXAEEb6SMvNqpAotC5-iyOss8aRFkLl9RojOp8shnBVJ1ESeyrR4huu9vwKKCymFMtCb_BAhrIxs5nzKcpApeEPzmzVOaNExNEiLQlOiThQ5Mq7aa7w&h=EvzVbq2_4ZvEJFpozbzTvEHSlEml3LvUocnKoyJ_Lvc" + ] + }, + "body": { + "string": "" + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Network/networkSecurityGroups?api-version=2024-05-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-throttle-levels": [ + "operationConcurrencyPct=0.5, subscriptionReadRatePct=0.2, etc" + ], + "x-ms-original-request-ids": [ + "4a3d3a2e-6246-43a6-ad96-7fbaead8535c" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-arm-service-request-id": [ + "379c2433-5cde-4c88-9303-aaaaf17dee89" + ], + "content-length": [ + "5173" + ], + "cache-control": [ + "no-cache" + ], + "x-msedge-ref": [ + "Ref A: 1BCB9CB92C7244D5A3FF51416ACB0264 Ref B: CHI261140505023 Ref C: 2026-09-28T20:26:36Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Mon, 28 Sep 2026 20:26:37 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "c7n-nsg-remove-rules", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups", + "location": "southcentralus", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "7a749cea-eac1-420b-9cc9-66b20cd394c5", + "securityRules": [], + "defaultSecurityRules": [ + { + "name": "AllowVnetInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowVnetInBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow inbound traffic from all VMs in VNET", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "VirtualNetwork", + "destinationAddressPrefix": "VirtualNetwork", + "access": "Allow", + "priority": 65000, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowAzureLoadBalancerInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowAzureLoadBalancerInBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow inbound traffic from azure load balancer", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "AzureLoadBalancer", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 65001, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "DenyAllInBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/DenyAllInBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Deny all inbound traffic", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Deny", + "priority": 65500, + "direction": "Inbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowVnetOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowVnetOutBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow outbound traffic from all VMs to all VMs in VNET", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "VirtualNetwork", + "destinationAddressPrefix": "VirtualNetwork", + "access": "Allow", + "priority": 65000, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "AllowInternetOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/AllowInternetOutBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Allow outbound traffic from all VMs to Internet", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "Internet", + "access": "Allow", + "priority": 65001, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + }, + { + "name": "DenyAllOutBound", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_networksecuritygroup-remove-rules/providers/Microsoft.Network/networkSecurityGroups/c7n-nsg-remove-rules/defaultSecurityRules/DenyAllOutBound", + "etag": "W/\"a446b369-ca57-4571-affb-c6864a34ac90\"", + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "properties": { + "provisioningState": "Succeeded", + "description": "Deny all outbound traffic", + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Deny", + "priority": 65500, + "direction": "Outbound", + "sourcePortRanges": [], + "destinationPortRanges": [], + "sourceAddressPrefixes": [], + "destinationAddressPrefixes": [] + } + } + ] + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/networksecuritygroup-remove-rules.json b/tools/c7n_azure/tests_azure/templates/networksecuritygroup-remove-rules.json new file mode 100644 index 00000000000..454236011a7 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/networksecuritygroup-remove-rules.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "networkSecurityGroups_c7n_nsg_name": { + "defaultValue": "c7n-nsg-remove-rules", + "type": "String" + } + }, + "variables": {}, + "resources": [ + { + "type": "Microsoft.Network/networkSecurityGroups", + "apiVersion": "2022-01-01", + "name": "[parameters('networkSecurityGroups_c7n_nsg_name')]", + "location": "[resourceGroup().location]", + "properties": { + "securityRules": [ + { + "name": "allow-all-inbound", + "properties": { + "protocol": "*", + "sourcePortRange": "*", + "destinationPortRange": "*", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 100, + "direction": "Inbound" + } + } + ] + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_networksecuritygroup.py b/tools/c7n_azure/tests_azure/tests_resources/test_networksecuritygroup.py index 8d222b00e47..0e928ece1a6 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_networksecuritygroup.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_networksecuritygroup.py @@ -1,6 +1,6 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 -from unittest.mock import patch +from unittest.mock import MagicMock, patch from parameterized import parameterized from pytest_terraform import terraform @@ -31,7 +31,8 @@ def test_network_security_group_schema_validate(self): {'type': 'close', 'ports': '1000-1100', 'direction': 'Inbound'}, - + {'type': 'remove-rules', + 'ingress': 'matched'}, ] }, validate=True) self.assertTrue(p) @@ -391,6 +392,120 @@ def test_deny_implicit(self): elif r['name'] == 'nsg-deny-all-ingress': self.assertEqual(matched_rules, {'deny-all-ingress'}) + @arm_template('networksecuritygroup-remove-rules.json') + def test_remove_rules_removes_matched_ingress_rule(self): + p = self.load_policy({ + 'name': 'test-azure-nsg', + 'resource': 'azure.networksecuritygroup', + 'filters': [ + {'type': 'value', + 'key': 'name', + 'op': 'eq', + 'value': 'c7n-nsg-remove-rules'}, + {'type': 'ingress', + 'source': '*', + 'access': 'Allow'}], + 'actions': [ + {'type': 'remove-rules', + 'ingress': 'matched'}], + }) + + resources = p.run() + + assert len(resources) == 1 + matched = resources[0]['c7n:matched-ingress-security-rules'] + assert [r['name'] for r in matched] == ['allow-all-inbound'] + + p = self.load_policy({ + 'name': 'test-azure-nsg', + 'resource': 'azure.networksecuritygroup', + 'filters': [ + {'type': 'value', + 'key': 'name', + 'op': 'eq', + 'value': 'c7n-nsg-remove-rules'}], + }) + + after = p.run() + + assert len(after) == 1 + assert after[0]['properties']['securityRules'] == [] + + def run_remove_rules(self, *rules, matched=(), **action): + """Remove rules from an NSG built from (name, priority, direction) specs. + + `matched` names the rules an ingress/egress filter would have annotated. + Returns the name of each rule the action asked Azure to delete. + """ + built = [{ + 'name': name, + 'id': '/subscriptions/xxx/securityRules/%s' % name, + 'properties': { + 'protocol': '*', + 'sourcePortRange': '*', + 'destinationPortRange': '*', + 'sourceAddressPrefix': '*', + 'destinationAddressPrefix': '*', + 'access': 'Allow', + 'priority': priority, + 'direction': direction, + }, + } for name, priority, direction in rules] + + nsg = { + 'name': 'nsg-test', + 'resourceGroup': 'test-rg', + 'properties': {'securityRules': built}, + } + for key, direction in (('c7n:matched-ingress-security-rules', 'Inbound'), + ('c7n:matched-egress-security-rules', 'Outbound')): + hits = [r for r in built + if r['name'] in matched and r['properties']['direction'] == direction] + if hits: + nsg[key] = hits + + p = self.load_policy({ + 'name': 'test-azure-nsg', + 'resource': 'azure.networksecuritygroup', + 'actions': [dict(type='remove-rules', **action)], + }) + remove = p.resource_manager.actions[0] + remove.manager.get_client = MagicMock() + remove.process([nsg]) + + deleted = remove.manager.get_client.return_value.security_rules.begin_delete + return [call[0][2] for call in deleted.call_args_list] + + def test_remove_rules_deletes_the_matched_rule(self): + deleted = self.run_remove_rules( + ('allow-ssh', 100, 'Inbound'), + ('allow-http', 200, 'Inbound'), + matched=('allow-ssh',), ingress='matched') + + assert deleted == ['allow-ssh'] + + def test_remove_rules_without_a_filter_deletes_nothing(self): + deleted = self.run_remove_rules( + ('allow-ssh', 100, 'Inbound'), ingress='matched') + + assert deleted == [] + + def test_remove_rules_all_only_touches_one_direction(self): + deleted = self.run_remove_rules( + ('in-1', 100, 'Inbound'), + ('in-2', 200, 'Inbound'), + ('out-1', 100, 'Outbound'), + ingress='all') + + assert sorted(deleted) == ['in-1', 'in-2'] + + def test_remove_rules_handles_egress(self): + deleted = self.run_remove_rules( + ('allow-rdp-out', 100, 'Outbound'), + matched=('allow-rdp-out',), egress='matched') + + assert deleted == ['allow-rdp-out'] + class NetworkSecurityGroupFlowLogsFilterTest(BaseTest): def test_flow_log_filter_all(self): From 81c4bcc150376191af85e684ef95bf926cc62435 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Wed, 30 Sep 2026 15:18:06 -0600 Subject: [PATCH 22/65] azure - add machine learning job cancel action (#11059) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../resources/machine_learning_job.py | 56 ++ tools/c7n_azure/tests_azure/azure_common.py | 14 +- ...Test.machine-learning-job-cancel-skip.json | 131 +++ ...ngJobTest.machine-learning-job-cancel.json | 857 ++++++++++++++++++ .../machine-learning-job-cancel.json | 156 ++++ .../test_machine_learning_job.py | 74 +- 6 files changed, 1285 insertions(+), 3 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel-skip.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel.json create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-job-cancel.json diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py index 8a969d407f6..8e0d5052817 100644 --- a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py @@ -43,6 +43,7 @@ class resource_type(ChildArmResourceManager.resource_type): enum_spec = ('jobs', 'list', None) parent_manager_name = 'machine-learning-workspace' resource_type = 'Microsoft.MachineLearningServices/workspaces/jobs' + raise_on_exception = False default_report_fields = ( 'name', 'resourceGroup', @@ -79,3 +80,58 @@ def _process_resource(self, resource): body=resource, ) return 'archived' + + +@MachineLearningJob.action_registry.register('cancel') +class MachineLearningJobCancelAction(AzureBaseAction): + """Cancel Azure Machine Learning jobs. + + Cancellation is asynchronous: the job moves to ``CancelRequested`` before + reaching the terminal ``Canceled`` status. Jobs that are already terminal, + or already cancelling, are skipped. + + :example: + + Cancel jobs whose heartbeat has timed out. + + .. code-block:: yaml + + policies: + - name: azure-ml-cancel-not-responding-jobs + resource: azure.machine-learning-job + filters: + - type: value + key: properties.status + value: NotResponding + actions: + - type: cancel + + """ + + schema = type_schema('cancel') + + uncancellable_statuses = frozenset(( + 'Completed', + 'Failed', + 'Canceled', + 'CancelRequested', + )) + + def _prepare_processing(self) -> None: + self.client = self.manager.get_client() + + def _process_resource(self, resource: dict) -> str: + status = resource['properties']['status'] + if status in self.uncancellable_statuses: + return f'not cancelled, status is {status}' + + workspace = ResourceIdParser.get_resource_name(resource['c7n:parent-id']) + self.client.jobs.begin_cancel( + resource_group_name=ResourceIdParser.get_resource_group(resource['id']), + workspace_name=workspace, + id=resource['name'], + # The default poller would spawn a thread per job to watch the + # cancellation through to Canceled; requesting it is enough. + polling=False, + ) + return 'cancel requested' diff --git a/tools/c7n_azure/tests_azure/azure_common.py b/tools/c7n_azure/tests_azure/azure_common.py index a195cb0de3d..255e81a979e 100644 --- a/tools/c7n_azure/tests_azure/azure_common.py +++ b/tools/c7n_azure/tests_azure/azure_common.py @@ -159,8 +159,10 @@ def __init__(self, *args, **kwargs): def setUp(self): super(AzureVCRBaseTest, self).setUp() - # is_playback() is only meaningful here: by cleanup time a recording - # run has written the cassette, making it indistinguishable from one. + + # When playing back, if we're in strict mode, make sure + # We played the whole cassette. Otherwise, the default, + # we may only play a subset. if self.is_playback() and getattr( getattr(self, self._testMethodName), 'strict_cassette', False): # Registered after the cassette's own cleanup, so it runs first, @@ -256,6 +258,14 @@ def _response_callback(self, response): response['headers'].items() if k.lower() not in self.FILTERED_HEADERS} + # Headers such as location and azure-asyncoperation carry the + # subscription id, and are not covered by the body scrubbing below. + response['headers'] = { + k: [self._replace_subscription_id(self._replace_tenant_id(value)) + for value in v] + for (k, v) in response['headers'].items() + } + if not response['body'].get('string'): return response diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel-skip.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel-skip.json new file mode 100644 index 00000000000..cd0ea66256d --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel-skip.json @@ -0,0 +1,131 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-completed-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-length": [ + "3118" + ], + "date": [ + "Wed, 30 Sep 2026 19:29:50 GMT" + ], + "x-request-time": [ + "0.079" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-msedge-ref": [ + "Ref A: 21F2C1D4549248469C108AC9607470ED Ref B: CYS013050701011 Ref C: 2026-09-30T19:29:50Z" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-ms-response-type": [ + "standard" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-completed-job", + "name": "cctest-completed-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-09-30 19:28:16", + "EndTimeUtc": "2026-09-30 19:29:03" + }, + "displayName": "cctest_completed_job", + "status": "Completed", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-completed-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-job-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/computes/cctestcpucancel", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-completed-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-30T19:25:21.2716411+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel.json new file mode 100644 index 00000000000..3e70eb38c93 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningJobTest.machine-learning-job-cancel.json @@ -0,0 +1,857 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:41 GMT" + ], + "content-length": [ + "10318" + ], + "x-msedge-ref": [ + "Ref A: 69E87A77E94F44DCA321BBAEB44AC6D8 Ref B: CYS013050703023 Ref C: 2026-09-30T20:07:40Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "name": "aml-delete-0812095033", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "westus", + "tags": { + "createdByToolkit": "cli-v2-1.34.1" + }, + "etag": null, + "properties": { + "friendlyName": "aml-delete-0812095033", + "description": "aml-delete-0812095033", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Storage/storageAccounts/amldeletstorage25aa71048", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.Keyvault/vaults/amldeletkeyvault5540f7e4", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.insights/components/amldeletinsights95d2b2da", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-08-12T14:03:06.5147287Z", + "notebookInfo": { + "resourceId": "67293dad521f4e8c82e6f3fe495b572e", + "fqdn": "ml-aml-delete-08120-westus-fee9f3af-061d-4d78-a2c7-7dc193975323.westus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fee9f3af-061d-4d78-a2c7-7dc193975323", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://westus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://westus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033", + "sdkTelemetryAppInsightsKey": "ad4db683-227d-4cf9-b788-db8d151f62e0", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "5088aaa7-547c-4b1a-a0f2-e5242bcfcf49", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-08-12T14:03:06.5147287Z", + "createdBy": "drea.jenkins@sixfeetup.com", + "createdByType": "User", + "lastModifiedAt": "2026-08-12T14:03:06.5147287Z", + "lastModifiedBy": "drea.jenkins@sixfeetup.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "name": "c7n-ml-mv-kcm-d1c9", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Storage/storageAccounts/c7nmlmvd1c9", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.Keyvault/vaults/c7n-ml-mv-kv-kcm-d1c9", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.insights/components/c7n-ml-mv-ai-kcm-d1c9", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-01T16:38:42.7908440Z", + "notebookInfo": { + "resourceId": "3149579492444da4bc75174b139b1445", + "fqdn": "ml-c7n-ml-mv-kcm-d1-eastus-3446780e-dd37-4558-9008-83cc4130dcaa.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "3446780e-dd37-4558-9008-83cc4130dcaa", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "34199801-a749-47e3-a28f-724fdb4c8904", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-01T16:38:42.790844Z", + "createdBy": "kody.mullins@sixfeetup.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-01T16:38:42.790844Z", + "lastModifiedBy": "kody.mullins@sixfeetup.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-792454565316", + "name": "cctest-mlws-792454565316", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-792454565316", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Storage/storageAccounts/cctestmlws792454565316", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.Keyvault/vaults/cctestmlws792454565316", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.insights/components/cctestmlws792454565316", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Deleting", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "softDeletedAt": "2026-09-10T21:24:37.2840812+00:00", + "scheduledPurgeDate": "2026-09-24T20:24:37.2840812+00:00", + "SoftDeleteUniqueId": "a53408c5-4afa-4eb8-9056-1db7eeffd51d", + "creationTime": "2026-09-10T21:13:40.1618675Z", + "notebookInfo": { + "resourceId": "7d20a9ae9c0e45e5a7b036393a7d9c5a", + "fqdn": "ml-cctest-mlws-7924-eastus-4e15b377-162d-44cc-b64d-78636a43b2ab.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "4e15b377-162d-44cc-b64d-78636a43b2ab", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-792454565316", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "414347b6-447a-4fab-b06f-875ead389b37", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-10T21:13:40.1618675Z", + "createdBy": "jim.fulton@sixfeetup.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-10T21:13:40.1618675Z", + "lastModifiedBy": "jim.fulton@sixfeetup.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel", + "name": "cctest-mlws-job-cancel", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-mlws-job-cancel", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Storage/storageAccounts/cctestmlws254523190822", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.Keyvault/vaults/cctestmlws254523190822", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.insights/components/cctestmlws254523190822", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-30T19:24:43.2405833Z", + "notebookInfo": { + "resourceId": "c6746ebb10664a64a9eaa8884a935db6", + "fqdn": "ml-cctest-m-southcentralus-fe7e7ec5-ed70-4c6f-bd46-dbeab74b3b60.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "fe7e7ec5-ed70-4c6f-bd46-dbeab74b3b60", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel", + "sdkTelemetryAppInsightsKey": "0c54a945-002c-433a-93ea-97b7f084292a", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "c7abeb74-576f-47f5-8e24-2d5fe0c08fef", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-30T19:24:43.2405833Z", + "createdBy": "jim.fulton@sixfeetup.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-30T19:24:43.2405833Z", + "lastModifiedBy": "jim.fulton@sixfeetup.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/aml-delete-test-0812095033/providers/Microsoft.MachineLearningServices/workspaces/aml-delete-0812095033/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-request-time": [ + "0.276" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:42 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "17" + ], + "x-msedge-ref": [ + "Ref A: C2D3BCE4273B46DBA94B410FEFCC1FDE Ref B: CYS013050701051 Ref C: 2026-09-30T20:07:41Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-westus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/stacklet-dev/providers/Microsoft.MachineLearningServices/workspaces/c7n-ml-mv-kcm-d1c9/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-request-time": [ + "0.379" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:43 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "17" + ], + "x-msedge-ref": [ + "Ref A: 755F4DA46D864237961E778F2D7B2EA8 Ref B: CYS013050702035 Ref C: 2026-09-30T20:07:43Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-792454565316/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 404, + "message": "Not Found" + }, + "headers": { + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:43 GMT" + ], + "content-length": [ + "117" + ], + "x-msedge-ref": [ + "Ref A: 88688D2191094CA992326B4DA7623D94 Ref B: CYS013050704037 Ref C: 2026-09-30T20:07:44Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-failure-cause": [ + "gateway" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "error": { + "code": "ResourceGroupNotFound", + "message": "Resource group 'test_machine-learning-job' could not be found." + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-request-time": [ + "0.311" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:45 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "6795" + ], + "x-msedge-ref": [ + "Ref A: D8FE00BDCD6F4B31B74299A7BE95FFF2 Ref B: CYS013050701023 Ref C: 2026-09-30T20:07:44Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-cancel-job", + "name": "cctest-cancel-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-09-30 19:47:55" + }, + "displayName": "cctest_cancel_job", + "status": "Running", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-cancel-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-job-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/computes/cctestcpucancel", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "sleep 3600", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-cancel-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-30T19:45:47.0673603+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-completed-job", + "name": "cctest-completed-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-09-30 19:28:16", + "EndTimeUtc": "2026-09-30 19:29:03" + }, + "displayName": "cctest_completed_job", + "status": "Completed", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-completed-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-job-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/computes/cctestcpucancel", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-completed-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-30T19:25:21.2716411+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-cancel-job/cancel?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "Accepted" + }, + "headers": { + "x-ms-async-operation-timeout": [ + "PT1H" + ], + "x-request-time": [ + "0.674" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:46 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "4" + ], + "x-msedge-ref": [ + "Ref A: 1AB1029027B542C4BBD872205909859A Ref B: CYS013050702009 Ref C: 2026-09-30T20:07:45Z" + ], + "cache-control": [ + "no-cache" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/locations/southcentralus/mfeOperationResults/jc:fe7e7ec5-ed70-4c6f-bd46-dbeab74b3b60:cctest-cancel-job?api-version=2023-04-01&t=639263956663929652&c=MIIHlDCCBnygAwIBAgIQEWqq0LXHm9_RoSQk3rtLtzANBgkqhkiG9w0BAQsFADA2MTQwMgYDVQQDEytDQ01FIEcxIFRMUyBSU0EgMjA0OCBTSEEyNTYgMjA0OSBXVVMyIENBIDAxMB4XDTI2MDkyMzA2MDIzOVoXDTI2MTIxODEyMDIzOVowQDE-MDwGA1UEAxM1YXN5bmNvcGVyYXRpb25zaWduaW5nY2VydGlmaWNhdGUubWFuYWdlbWVudC5henVyZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTL3s1G4HRJxCxQWgaa1QhZF9rCHBTzLl8phyc14DtHqFD1UEEvU8TdKIkn63TfPvm-CD5nDTQJhmqP3TQ4a1zbeIZvN26UwO-f2zs8v284_6vqmQYpp074W-XCE3yUu6xZi0c2RjrWXRE0C83U9RwiZBkm4sAg6EaNrBTlMtlbtByJMhA9KOAfS_ME_eoDeS2P_o__hD4yg538D4itpA1Yp3-XcF3vu3MgpyPUf4o8rU8J3Y9-jIPfdgYdhE-QPuKdwKQu0Aov3aNNHzv_aJivc4z22sXdAC9MeLP59Q0e1QaF_moRqk0Sr9AyE7J1ohMafagWSkQ-fj_5H83r6J9AgMBAAGjggSSMIIEjjCBnQYDVR0gBIGVMIGSMAwGCisGAQQBgjd7AQEwZgYKKwYBBAGCN3sCAjBYMFYGCCsGAQUFBwICMEoeSAAzADMAZQAwADEAOQAyADEALQA0AGQANgA0AC0ANABmADgAYwAtAGEAMAA1ADUALQA1AGIAZABhAGYAZgBkADUAZQAzADMAZDAMBgorBgEEAYI3ewMCMAwGCisGAQQBgjd7BAIwDAYDVR0TAQH_BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDgYDVR0PAQH_BAQDAgWgMB0GA1UdDgQWBBTI83RcASQN5gnS06-jdiNGf7b_JDAfBgNVHSMEGDAWgBSs43L6A7Jznj2VyO-HW67dG6HtaDCCAbIGA1UdHwSCAakwggGlMGmgZ6BlhmNodHRwOi8vcHJpbWFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwa6BpoGeGZWh0dHA6Ly9zZWNvbmRhcnktY2RuLnBraS5jb3JlLndpbmRvd3MubmV0L3dlc3R1czIvY3Jscy9jY21ld2VzdHVzMnBraS9jY21ld2VzdHVzMmljYTAxLzk3L2N1cnJlbnQuY3JsMFqgWKBWhlRodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vd2VzdHVzMi9jcmxzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvOTcvY3VycmVudC5jcmwwb6BtoGuGaWh0dHA6Ly9jY21ld2VzdHVzMnBraS53ZXN0dXMyLnBraS5jb3JlLndpbmRvd3MubmV0L2NlcnRpZmljYXRlQXV0aG9yaXRpZXMvY2NtZXdlc3R1czJpY2EwMS85Ny9jdXJyZW50LmNybDCCAbcGCCsGAQUFBwEBBIIBqTCCAaUwbAYIKwYBBQUHMAKGYGh0dHA6Ly9wcmltYXJ5LWNkbi5wa2kuY29yZS53aW5kb3dzLm5ldC93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBuBggrBgEFBQcwAoZiaHR0cDovL3NlY29uZGFyeS1jZG4ucGtpLmNvcmUud2luZG93cy5uZXQvd2VzdHVzMi9jYWNlcnRzL2NjbWV3ZXN0dXMycGtpL2NjbWV3ZXN0dXMyaWNhMDEvY2VydC5jZXIwXQYIKwYBBQUHMAKGUWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS93ZXN0dXMyL2NhY2VydHMvY2NtZXdlc3R1czJwa2kvY2NtZXdlc3R1czJpY2EwMS9jZXJ0LmNlcjBmBggrBgEFBQcwAoZaaHR0cDovL2NjbWV3ZXN0dXMycGtpLndlc3R1czIucGtpLmNvcmUud2luZG93cy5uZXQvY2VydGlmaWNhdGVBdXRob3JpdGllcy9jY21ld2VzdHVzMmljYTAxMA0GCSqGSIb3DQEBCwUAA4IBAQBtlTS-mdvGIKziCDp4X9OVPqRLuoH48ib0L8ZVUxxNXw-O3sl-lio6Y7V0Xqv47B1Ru94KfFJZpXsF6u0Gk7IcfpKl2PltvEAmAKSdpsOElmGW0MeH-AsmLU-uRKqaIGsDrnsyJuLqtILSJ_GCMnG1VbnaqOJFmoBMj7f_unbCkVhb6MmyJC4sd_klisVbuUnjtp_Cgp2Wc_HeRKPemyn8alnsmHVOO0NHWIR41o6dNQCt7LJ5nM5rQd2Fww6uP0eqn3jQiNE0nl_zOqY8Me96xb0gWGe9D4aRXyAG_j-B4_gTga7SVfy92csbUhBWi9huq3gKSoRRpuS_4dQqvDVj&s=XOOH574pjKdubtowDiJZrVDWkLg9nO4MHNz8TjhQK66p7Pzm7C1F6Ne6j7dSo3sPbJQ8S10f_quEIBGyUisaOgCjnHWmw_RNNEiJhUzqztjg-MERY2ibB_e6nOHJy-pDyrt6AgsUWta8xkCEf9WX0XlMpiPd7b1B7U_WklFpyJcu-5X6yhMZ1SwA_0HMUq2jXetdwpTdQR_K3jITu6LY1u5x0-ZBz3KsE1X3tGmfwYQTOQZVS2Wvs3Egu5WZoTXkrzgNRsBItk2LZoY82UzySAL_onllMf9oEuTDUdvuBYxBmSW4lZFSCCOoXLDFcz7BJOPpHAAmXBLhzfDttZ8liA&h=deoOUDMm50z_DTFBdLgKVnUtxQ_yZgvgdk8XxqSiNa4" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ] + }, + "body": { + "data": null + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-cancel-job?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-request-time": [ + "0.098" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Wed, 30 Sep 2026 20:07:46 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "3066" + ], + "x-msedge-ref": [ + "Ref A: F9EC2B51EF51405FB9783885AE46B326 Ref B: CYS013050704045 Ref C: 2026-09-30T20:07:46Z" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/jobs/cctest-cancel-job", + "name": "cctest-cancel-job", + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "properties": { + "description": null, + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctestcpucancel", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json", + "StartTimeUtc": "2026-09-30 19:47:55" + }, + "displayName": "cctest_cancel_job", + "status": "CancelRequested", + "experimentName": "cctest_cancel", + "services": { + "Tracking": { + "jobServiceType": "Tracking", + "port": null, + "endpoint": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel?", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + }, + "Studio": { + "jobServiceType": "Studio", + "port": null, + "endpoint": "https://ml.azure.com/runs/cctest-cancel-job?wsid=/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourcegroups/test_machine-learning-job-cancel/workspaces/cctest-mlws-job-cancel", + "status": null, + "errorMessage": null, + "properties": null, + "nodes": null + } + }, + "computeId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/computes/cctestcpucancel", + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "sleep 3600", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-job-cancel/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-job-cancel/environments/cctest-cancel-env/versions/1", + "inputs": {}, + "outputs": { + "default": { + "description": null, + "uri": "azureml://datastores/workspaceartifactstore/ExperimentRun/dcid.cctest-cancel-job", + "mode": "ReadWriteMount", + "jobOutputType": "uri_folder" + } + }, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": {} + }, + "systemData": { + "createdAt": "2026-09-30T19:45:47.0673603+00:00", + "createdBy": "Jim Fulton", + "createdByType": "User" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-job-cancel.json b/tools/c7n_azure/tests_azure/templates/machine-learning-job-cancel.json new file mode 100644 index 00000000000..0731fac8839 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-job-cancel.json @@ -0,0 +1,156 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + } + }, + "variables": { + "storageAccountName": "[concat('cctestmlws', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestmlws', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestmlws', parameters('uniqueId'))]", + "workspaceName": "cctest-mlws-job-cancel", + "computeName": "cctestcpucancel", + "environmentName": "cctest-cancel-env", + "jobName": "cctest-cancel-job", + "completedJobName": "cctest-completed-job" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-01-01", + "name": "[variables('storageAccountName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false + } + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2023-02-01", + "name": "[variables('keyVaultName')]", + "location": "[resourceGroup().location]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [] + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[resourceGroup().location]", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[variables('workspaceName')]", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('computeName'))]", + "location": "[resourceGroup().location]", + "properties": { + "computeLocation": "[resourceGroup().location]", + "computeType": "AmlCompute", + "disableLocalAuth": true, + "properties": { + "enableNodePublicIp": true, + "osType": "Linux", + "remoteLoginPortPublicAccess": "Enabled", + "scaleSettings": { + "maxNodeCount": 2, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "vmPriority": "Dedicated", + "vmSize": "Standard_DS3_v2" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}/1', variables('workspaceName'), variables('environmentName'))]", + "properties": { + "environmentType": "UserCreated", + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04", + "isArchived": false, + "osType": "Linux", + "properties": { + "azureml.labels": "latest" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('jobName'))]", + "properties": { + "command": "sleep 3600", + "computeId": "[resourceId('Microsoft.MachineLearningServices/workspaces/computes', variables('workspaceName'), variables('computeName'))]", + "displayName": "cctest_cancel_job", + "environmentId": "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]", + "experimentName": "cctest_cancel", + "jobType": "Command" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/computes', variables('workspaceName'), variables('computeName'))]", + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/jobs", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('completedJobName'))]", + "properties": { + "command": "echo hello", + "computeId": "[resourceId('Microsoft.MachineLearningServices/workspaces/computes', variables('workspaceName'), variables('computeName'))]", + "displayName": "cctest_completed_job", + "environmentId": "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]", + "experimentName": "cctest_cancel", + "jobType": "Command" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/computes', variables('workspaceName'), variables('computeName'))]", + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py index 4683d23341c..98141f2efda 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py @@ -6,10 +6,16 @@ from c7n_azure.resources.machine_learning_job import ( MachineLearningJob, MachineLearningJobArchiveAction, + MachineLearningJobCancelAction, ) from c7n_azure.session import Session from c7n_azure.utils import ResourceIdParser -from ..azure_common import BaseTest, arm_template, cassette_name +from ..azure_common import ( + BaseTest, + arm_template, + cassette_name, + strict_cassette, + ) class MachineLearningJobTest(BaseTest): @@ -43,6 +49,13 @@ def test_machine_learning_job_schema_validate(self): }, validate=True) self.assertTrue(p) + p = self.load_policy({ + 'name': 'cancel-machine-learning-jobs', + 'resource': 'azure.machine-learning-job', + 'actions': [{'type': 'cancel'}], + }, validate=True) + self.assertTrue(p) + for action in ('tag', 'untag', 'auto-tag-user', 'auto-tag-date', 'tag-trim', 'mark-for-op'): self.assertNotIn(action, MachineLearningJob.action_registry) @@ -176,3 +189,62 @@ def test_machine_learning_job_archive_skips_archived_job(self): action._prepare_processing() self.assertEqual('already archived', action._process_resource(resource)) + + @arm_template('machine-learning-job-cancel.json') + @strict_cassette('machine-learning-job-cancel') + def test_machine_learning_job_cancel(self): + p = self.load_policy({ + 'name': 'cancel-machine-learning-job', + 'resource': 'azure.machine-learning-job', + 'filters': [{ + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-job-cancel', + }, { + 'type': 'value', + 'key': 'name', + 'value': 'cctest-cancel-job', + }], + 'actions': [{'type': 'cancel'}], + }, validate=True, session_factory=Session) + + resources = p.run() + self.assertEqual(1, len(resources)) + + client = local_session(Session).client( + 'azure.mgmt.machinelearningservices.MachineLearningServicesMgmtClient') + resource_group = ResourceIdParser.get_resource_group(resources[0]['id']) + workspace = ResourceIdParser.get_resource_name(resources[0]['c7n:parent-id']) + job = client.jobs.get(resource_group, workspace, resources[0]['name']) + # Cancellation is requested without waiting, so a live run can still + # see the job running for a moment after the request is accepted. + while job.properties.status not in ( + 'CancelRequested', 'Canceled', 'Completed', 'Failed'): + self.sleep_in_live_mode(30) + job = client.jobs.get(resource_group, workspace, resources[0]['name']) + + self.assertIn(job.properties.status, ('CancelRequested', 'Canceled')) + + @arm_template('machine-learning-job-cancel.json') + @strict_cassette('machine-learning-job-cancel-skip') + def test_machine_learning_job_cancel_skips_completed_job(self): + p = self.load_policy({ + 'name': 'cancel-machine-learning-job', + 'resource': 'azure.machine-learning-job', + 'actions': [{'type': 'cancel'}], + }, validate=True, session_factory=Session) + client = local_session(Session).client( + 'azure.mgmt.machinelearningservices.MachineLearningServicesMgmtClient') + job = client.jobs.get( + 'test_machine-learning-job-cancel', + 'cctest-mlws-job-cancel', + 'cctest-completed-job', + ) + resource = _serialize(job) + + self.assertEqual('Completed', resource['properties']['status']) + action = MachineLearningJobCancelAction({'type': 'cancel'}, p.resource_manager) + action._prepare_processing() + + self.assertEqual( + 'not cancelled, status is Completed', action._process_resource(resource)) From 5541cb971d0516a60338421fa8ef1cf6427256c7 Mon Sep 17 00:00:00 2001 From: Tim Penhey Date: Fri, 2 Oct 2026 05:10:22 +1300 Subject: [PATCH 23/65] fix(kube) - include the resource type in the resource cache key (#11168) QueryResourceManager.get_cache_key keyed the resource cache on the source type and query alone, so every Kubernetes resource type mapped to the same cache entry. With caching enabled, the first policy to run populated it and every later policy within the cache period was handed that policy's resources instead of querying its own: a k8s.deployment policy run after a k8s.pod policy filtered the cluster's pods, matched nothing, and reported an empty result without error. Key on the resource manager's class as well, as c7n.query's QueryResourceManager does. Custom resources were already distinct, since their group, version and plural are part of the query. --------- Co-authored-by: AJ Kerrigan Co-authored-by: Claude Opus 5.5 (1M context) --- tools/c7n_kube/c7n_kube/query.py | 6 +++++- tools/c7n_kube/tests/test_kube.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/c7n_kube/c7n_kube/query.py b/tools/c7n_kube/c7n_kube/query.py index 3f0d1a34d48..1281b14ec91 100644 --- a/tools/c7n_kube/c7n_kube/query.py +++ b/tools/c7n_kube/c7n_kube/query.py @@ -113,7 +113,11 @@ def get_model(self): return self.resource_type def get_cache_key(self, query): - return {"source_type": self.source_type, "query": query} + return { + "resource": self.__class__.__name__, + "source_type": self.source_type, + "query": query, + } @property def source_type(self): diff --git a/tools/c7n_kube/tests/test_kube.py b/tools/c7n_kube/tests/test_kube.py index 7700cb70ea7..5de0db0f1b9 100644 --- a/tools/c7n_kube/tests/test_kube.py +++ b/tools/c7n_kube/tests/test_kube.py @@ -34,3 +34,23 @@ def test_kube_cache(self): call("Filtered from 5 to 5 namespace"), ] p.resource_manager.log.debug.assert_has_calls(calls) + + def test_kube_cache_keyed_by_resource_type(self): + # Two resource types sharing one cache must not be handed each other's + # resources. + pod = self.load_policy({"name": "pods", "resource": "k8s.pod"}, cache=True) + deployment = self.load_policy( + {"name": "deployments", "resource": "k8s.deployment"}, config=pod.options + ) + for p, kind in ((pod, "Pod"), (deployment, "Deployment")): + # Close each cache connection before the temp dir is removed, as + # windows won't delete a file that is still open. + self.addCleanup(p.resource_manager._cache.close) + self.patch( + p.resource_manager.source, + "get_resources", + lambda query, kind=kind: [{"kind": kind}], + ) + + self.assertEqual(pod.resource_manager.resources(), [{"kind": "Pod"}]) + self.assertEqual(deployment.resource_manager.resources(), [{"kind": "Deployment"}]) From d02cc3d39f870cf678b7bb13b7f1058ed6214b0c Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 1 Oct 2026 16:35:07 -0400 Subject: [PATCH 24/65] core - resolver - include request headers in uri cache key (#11148) --- c7n/resolver.py | 29 ++++++++++++++--- tests/test_resolver.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/c7n/resolver.py b/c7n/resolver.py index acce3bdf6c6..c3c370c50c8 100644 --- a/c7n/resolver.py +++ b/c7n/resolver.py @@ -1,6 +1,7 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 import csv +import hashlib import io import json import os.path @@ -37,8 +38,28 @@ def register_provider(cls, scheme, handler): """ cls._uri_providers[scheme] = handler + def get_cache_key(self, uri, headers): + """Build the cache key for a uri fetch. + + Headers can carry per-caller authorization, so a response fetched + with one set of credentials must not be served to a caller + presenting different ones. Headers are hashed rather than embedded + to keep credentials out of the on-disk cache. Without headers the + key keeps its original form, so existing caches stay valid. + """ + if not headers: + return ("uri-resolver", uri) + # header names are case insensitive, canonicalize before hashing + canonical = json.dumps( + sorted((k.lower(), v) for k, v in headers.items()), + separators=(',', ':'), + ) + digest = hashlib.sha256(canonical.encode('utf8')).hexdigest() + return ("uri-resolver", uri, digest) + def resolve(self, uri, headers): - contents = self.cache.get(("uri-resolver", uri)) + cache_key = self.get_cache_key(uri, headers) + contents = self.cache.get(cache_key) if contents is not None: return contents @@ -58,8 +79,8 @@ def resolve(self, uri, headers): contents = self.get_s3_uri(uri) elif scheme in ('http', 'https', 'file'): # Standard URL schemes handled by urllib - headers.update({"Accept-Encoding": "gzip"}) - req = Request(uri, headers=headers) + # copy rather than mutate, the caller's dict keys the cache + req = Request(uri, headers={**headers, "Accept-Encoding": "gzip"}) with closing(urlopen(req)) as response: # nosec nosemgrep contents = self.handle_response_encoding(response) elif scheme in self._uri_providers: @@ -74,7 +95,7 @@ def resolve(self, uri, headers): f"Registered provider schemes: {provider_schemes}" ) - self.cache.save(("uri-resolver", uri), contents) + self.cache.save(cache_key, contents) return contents def handle_response_encoding(self, response): diff --git a/tests/test_resolver.py b/tests/test_resolver.py index f38e36fd88f..058bf350be5 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -135,6 +135,79 @@ def test_resolve_file(self): fh.flush() self.assertEqual(resolver.resolve("file:%s" % fh.name, {'auth': 'token'}), content) + def test_resolve_cache_key_includes_headers(self): + # a response fetched with one set of credentials must not be + # served to a caller presenting different ones + cache = FakeCache() + resolver = URIResolver(None, cache) + + alice_content = json.dumps({"tenant": "alice"}) + bob_content = json.dumps({"tenant": "bob"}) + + with tempfile.NamedTemporaryFile(mode="w+", dir=os.getcwd(), delete=False) as fh: + self.addCleanup(os.unlink, fh.name) + fh.write(alice_content) + fh.flush() + uri = "file:%s" % fh.name + + self.assertEqual( + resolver.resolve(uri, {"authorization": "alice-token"}), + alice_content, + ) + + # rewrite the file so a cache hit is distinguishable from a fetch + fh.seek(0) + fh.truncate() + fh.write(bob_content) + fh.flush() + + self.assertEqual( + resolver.resolve(uri, {"authorization": "bob-token"}), + bob_content, + ) + + def test_resolve_cache_hit_on_identical_headers(self): + # same uri and same headers is still a cache hit + cache = FakeCache() + resolver = URIResolver(None, cache) + content = json.dumps({"tenant": "alice"}) + + fh = tempfile.NamedTemporaryFile(mode="w+", dir=os.getcwd(), delete=False) + fh.write(content) + fh.flush() + fh.close() + uri = "file:%s" % fh.name + + headers = {"authorization": "alice-token"} + self.assertEqual(resolver.resolve(uri, dict(headers)), content) + self.assertEqual(cache.saves, 1) + + # without the file a fetch would fail, so content proves a cache hit + os.unlink(fh.name) + self.assertEqual(resolver.resolve(uri, dict(headers)), content) + self.assertEqual(cache.saves, 1) + + def test_resolve_does_not_mutate_headers(self): + # ValuesFrom reuses one headers dict across calls, mutating it + # would change the cache key and defeat caching + cache = FakeCache() + resolver = URIResolver(None, cache) + content = json.dumps({"tenant": "alice"}) + + fh = tempfile.NamedTemporaryFile(mode="w+", dir=os.getcwd(), delete=False) + self.addCleanup(os.unlink, fh.name) + fh.write(content) + fh.flush() + fh.close() + uri = "file:%s" % fh.name + + headers = {"authorization": "alice-token"} + resolver.resolve(uri, headers) + self.assertEqual(headers, {"authorization": "alice-token"}) + + resolver.resolve(uri, headers) + self.assertEqual(cache.saves, 1) + def test_value_from_sqlkv(tmp_path): From ec8d701b0cebc234d5e196a61afb2dae5bf1f860 Mon Sep 17 00:00:00 2001 From: AJ Kerrigan Date: Thu, 1 Oct 2026 16:35:39 -0400 Subject: [PATCH 25/65] releng - prep 0.9.53 release (#11162) --------- Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/functional.yaml | 2 +- .github/workflows/release.yml | 2 +- c7n/version.py | 2 +- docker/c7n | 2 +- docker/c7n-kube | 2 +- docker/c7n-left | 4 +- docker/c7n-org | 2 +- docker/mailer | 2 +- docker/policystream | 2 +- pyproject.toml | 5 +- tools/c7n_awscc/pyproject.toml | 2 +- tools/c7n_azure/pyproject.toml | 4 +- tools/c7n_gcp/pyproject.toml | 2 +- tools/c7n_kube/pyproject.toml | 2 +- tools/c7n_left/pyproject.toml | 2 +- tools/c7n_mailer/pyproject.toml | 2 +- tools/c7n_oci/pyproject.toml | 2 +- tools/c7n_openstack/pyproject.toml | 2 +- tools/c7n_org/pyproject.toml | 2 +- tools/c7n_policystream/pyproject.toml | 2 +- tools/c7n_sphinxext/pyproject.toml | 2 +- tools/c7n_tencentcloud/pyproject.toml | 2 +- tools/dev/dockerpkg.py | 2 +- uv.lock | 2532 +++++++++++++------------ 25 files changed, 1371 insertions(+), 1216 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b9eebf7f5c..979543db29c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: - UV_VERSION: "0.7.12" + UV_VERSION: "0.12.19" DEFAULT_PY_VERSION: "3.12" permissions: {} diff --git a/.github/workflows/functional.yaml b/.github/workflows/functional.yaml index 75f461a3960..7cc32bc86a7 100644 --- a/.github/workflows/functional.yaml +++ b/.github/workflows/functional.yaml @@ -4,7 +4,7 @@ on: workflow_dispatch: {} env: - UV_VERSION: "0.7.12" + UV_VERSION: "0.12.19" DEFAULT_PY_VERSION: "3.12" permissions: {} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d947d551816..aaf4882c2d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: "Release" on: workflow_dispatch: {} env: - UV_VERSION: "0.7.12" + UV_VERSION: "0.12.19" DEFAULT_PY_VERSION: "3.12" permissions: {} diff --git a/c7n/version.py b/c7n/version.py index e9279ce8bde..3355b4fdbc1 100644 --- a/c7n/version.py +++ b/c7n/version.py @@ -1,2 +1,2 @@ # Generated via tools/dev/devpkg.py -version = "0.9.52" +version = "0.9.53" diff --git a/docker/c7n b/docker/c7n index 58b0aa801fa..a2f600bbf3a 100644 --- a/docker/c7n +++ b/docker/c7n @@ -9,7 +9,7 @@ RUN apt-get --yes install --no-install-recommends build-essential curl pytho RUN adduser --disabled-login --gecos "" custodian # wheel installation cache RUN --mount=type=cache,target=/root/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.12.19 /uv /uvx /bin/ ARG PATH="/root/.local/bin:$PATH" WORKDIR /src diff --git a/docker/c7n-kube b/docker/c7n-kube index b0b73b35067..debbf325aa7 100644 --- a/docker/c7n-kube +++ b/docker/c7n-kube @@ -9,7 +9,7 @@ RUN apt-get --yes install --no-install-recommends build-essential curl pytho RUN adduser --disabled-login --gecos "" custodian # wheel installation cache RUN --mount=type=cache,target=/root/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.12.19 /uv /uvx /bin/ ARG PATH="/root/.local/bin:$PATH" WORKDIR /src diff --git a/docker/c7n-left b/docker/c7n-left index 2f280070d23..480ea7de269 100644 --- a/docker/c7n-left +++ b/docker/c7n-left @@ -1,6 +1,6 @@ FROM cgr.dev/chainguard/wolfi-base AS builder -ARG UV_VERSION="0.7.6" +ARG UV_VERSION="0.12.19" ARG PY_VERSION=3.12 WORKDIR /app @@ -10,7 +10,7 @@ USER nonroot # Install uv and mount wheel cache RUN --mount=type=cache,target=/home/nonroot/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /uvx /bin/ ENV UV_PROJECT_ENVIRONMENT=/home/nonroot/venv # Copy enough of the c7n source that poetry can diff --git a/docker/c7n-org b/docker/c7n-org index 7e500482381..cb62ca47387 100644 --- a/docker/c7n-org +++ b/docker/c7n-org @@ -9,7 +9,7 @@ RUN apt-get --yes install --no-install-recommends build-essential curl pytho RUN adduser --disabled-login --gecos "" custodian # wheel installation cache RUN --mount=type=cache,target=/root/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.12.19 /uv /uvx /bin/ ARG PATH="/root/.local/bin:$PATH" WORKDIR /src diff --git a/docker/mailer b/docker/mailer index 274eee28064..9a3caa51c1f 100644 --- a/docker/mailer +++ b/docker/mailer @@ -9,7 +9,7 @@ RUN apt-get --yes install --no-install-recommends build-essential curl pytho RUN adduser --disabled-login --gecos "" custodian # wheel installation cache RUN --mount=type=cache,target=/root/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.12.19 /uv /uvx /bin/ ARG PATH="/root/.local/bin:$PATH" WORKDIR /src diff --git a/docker/policystream b/docker/policystream index d834d648b9a..a7c372d65cc 100644 --- a/docker/policystream +++ b/docker/policystream @@ -9,7 +9,7 @@ RUN apt-get --yes install --no-install-recommends build-essential curl pytho RUN adduser --disabled-login --gecos "" custodian # wheel installation cache RUN --mount=type=cache,target=/root/.cache/uv -COPY --from=ghcr.io/astral-sh/uv:0.7.6 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.12.19 /uv /uvx /bin/ ARG PATH="/root/.local/bin:$PATH" WORKDIR /src diff --git a/pyproject.toml b/pyproject.toml index f5a38476921..b822efd3c13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "urllib3>2", ] name = "c7n" -version = "0.9.52" +version = "0.9.53" description = "Cloud Custodian - Policy Rules Engine" readme = "README.md" classifiers = [ @@ -65,6 +65,9 @@ addons = [ "psutil>=5.7", ] +[tool.uv] +exclude-newer = "5 days" + [tool.uv.sources] c7n = { workspace = true } diff --git a/tools/c7n_awscc/pyproject.toml b/tools/c7n_awscc/pyproject.toml index cb8dd052340..acfa5408695 100644 --- a/tools/c7n_awscc/pyproject.toml +++ b/tools/c7n_awscc/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "jsonpatch<2.0,>=1.32", ] name = "c7n_awscc" -version = "0.1.37" +version = "0.1.38" readme = "readme.md" description = "Cloud Custodian - AWS Cloud Control Provider" classifiers = [ diff --git a/tools/c7n_azure/pyproject.toml b/tools/c7n_azure/pyproject.toml index c992002a4bb..e849c25a660 100644 --- a/tools/c7n_azure/pyproject.toml +++ b/tools/c7n_azure/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "azure-mgmt-dataprotection<2.0.0,>=1.4.0", "azure-mgmt-desktopvirtualization<2.0.0,>=1.0.0", "azure-mgmt-dns==8.0.0b1", - "azure-mgmt-eventgrid<11.0.0,>=10.3.0b4", + "azure-mgmt-eventgrid<11.0.0,>=10.5.0b2", "azure-mgmt-eventhub<12.0.0,>=11.0.0", "azure-mgmt-frontdoor<2.0.0,>=1.0.0", "azure-mgmt-hdinsight<8.0.0,>=7.0.0", @@ -89,7 +89,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_azure" -version = "0.7.51" +version = "0.7.52" description = "Cloud Custodian - Azure Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_gcp/pyproject.toml b/tools/c7n_gcp/pyproject.toml index fc9f3da21a3..e5eec3c7cba 100644 --- a/tools/c7n_gcp/pyproject.toml +++ b/tools/c7n_gcp/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "retrying<2.0.0,>=1.3.3", ] name = "c7n_gcp" -version = "0.4.51" +version = "0.4.52" description = "Cloud Custodian - Google Cloud Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_kube/pyproject.toml b/tools/c7n_kube/pyproject.toml index ed6ee2e98f5..738d81cb0df 100644 --- a/tools/c7n_kube/pyproject.toml +++ b/tools/c7n_kube/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "kubernetes>=10.0.1", ] name = "c7n_kube" -version = "0.2.51" +version = "0.2.52" description = "Cloud Custodian - Kubernetes Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_left/pyproject.toml b/tools/c7n_left/pyproject.toml index 8bd202c7d08..8d74a4e2f79 100644 --- a/tools/c7n_left/pyproject.toml +++ b/tools/c7n_left/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tfparse<1.0,>=0.6", ] name = "c7n_left" -version = "0.3.38" +version = "0.3.39" description = "Custodian policies for IAAC definitions" readme = "README.md" classifiers = [ diff --git a/tools/c7n_mailer/pyproject.toml b/tools/c7n_mailer/pyproject.toml index 4f383452c8e..a192b6b3836 100644 --- a/tools/c7n_mailer/pyproject.toml +++ b/tools/c7n_mailer/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "types-six<2.0.0,>=1.16.10", ] name = "c7n_mailer" -version = "0.6.51" +version = "0.6.52" description = "Cloud Custodian - Reference Mailer" readme = "README.md" classifiers = [ diff --git a/tools/c7n_oci/pyproject.toml b/tools/c7n_oci/pyproject.toml index b7f3c63747d..8eb32f6727a 100644 --- a/tools/c7n_oci/pyproject.toml +++ b/tools/c7n_oci/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "pyopenssl>=24" ] name = "c7n_oci" -version = "0.1.26" +version = "0.1.27" description = "Cloud Custodian - OCI Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_openstack/pyproject.toml b/tools/c7n_openstack/pyproject.toml index afff94a22e3..bfe2a234cdc 100644 --- a/tools/c7n_openstack/pyproject.toml +++ b/tools/c7n_openstack/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "openstacksdk==0.52.0", ] name = "c7n_openstack" -version = "0.1.42" +version = "0.1.43" description = "Cloud Custodian - OpenStack Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_org/pyproject.toml b/tools/c7n_org/pyproject.toml index 79ac6ecf8bf..3e54880b8e7 100644 --- a/tools/c7n_org/pyproject.toml +++ b/tools/c7n_org/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "click>=8.0", ] name = "c7n_org" -version = "0.6.51" +version = "0.6.52" description = "Cloud Custodian - Parallel Execution" readme = "README.md" classifiers = [ diff --git a/tools/c7n_policystream/pyproject.toml b/tools/c7n_policystream/pyproject.toml index 11f24383b14..87d422d802b 100644 --- a/tools/c7n_policystream/pyproject.toml +++ b/tools/c7n_policystream/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_policystream" -version = "0.4.51" +version = "0.4.52" description = "Cloud Custodian - Git Commits as Logical Policy Changes" readme = "README.md" classifiers = [ diff --git a/tools/c7n_sphinxext/pyproject.toml b/tools/c7n_sphinxext/pyproject.toml index cadd9fb7008..43ddb4d4b1e 100644 --- a/tools/c7n_sphinxext/pyproject.toml +++ b/tools/c7n_sphinxext/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "sphinx-rtd-theme>=1.0.0", ] name = "c7n_sphinxext" -version = "1.1.51" +version = "1.1.52" description = "Cloud Custodian - Sphinx Extensions" classifiers = [ "License :: OSI Approved :: Apache Software License", diff --git a/tools/c7n_tencentcloud/pyproject.toml b/tools/c7n_tencentcloud/pyproject.toml index e8eca1f2610..cd87ee2c8ab 100644 --- a/tools/c7n_tencentcloud/pyproject.toml +++ b/tools/c7n_tencentcloud/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tencentcloud-sdk-python<4.0.0,>=3.0.783", ] name = "c7n_tencentcloud" -version = "0.1.34" +version = "0.1.35" description = "Cloud Custodian - Tencent Cloud Provider" readme = "readme.md" diff --git a/tools/dev/dockerpkg.py b/tools/dev/dockerpkg.py index 402a5756785..f708df82d67 100644 --- a/tools/dev/dockerpkg.py +++ b/tools/dev/dockerpkg.py @@ -182,7 +182,7 @@ class Image: defaults = dict( base_build_image="ubuntu:24.04", base_target_image="ubuntu:24.04", - uv_version="0.7.6", + uv_version="0.12.19", packages="", providers=" ".join(default_providers), pre_entry="", diff --git a/uv.lock b/uv.lock index fde3792e35a..7c8f4a7a541 100644 --- a/uv.lock +++ b/uv.lock @@ -9,6 +9,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P5D" + [manifest] members = [ "c7n", @@ -312,15 +316,15 @@ wheels = [ [[package]] name = "azure-cosmos" -version = "4.16.4" +version = "4.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/8a/97b68e849fff7d2a76d85e51acdfbd13072266d581dcb7f6ad32bdddbd27/azure_cosmos-4.16.4.tar.gz", hash = "sha256:5a98d33ce6d539a926f47c67b14587fbb8ba4fd662751ff8b0ebe7f9d3765a2e", size = 2462258, upload-time = "2026-09-02T14:59:54.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/6d/5604820fa3417d98e61c57d07447500fce600d10695bed02f396c2d548b7/azure_cosmos-4.17.1.tar.gz", hash = "sha256:d692928a54d88163e96d41d8f899229ca2f6a6bf21973ece2bfc1f72d4b4edd8", size = 2494950, upload-time = "2026-09-16T22:21:15.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/cc/5f4b06f49ec0bf26e8a390fb55c67ceb67573e94e83493b6b96837b2ee98/azure_cosmos-4.16.4-py3-none-any.whl", hash = "sha256:89bf558203335efc39551dccef6782aa2f286d94cc87dd9bd98efc37d8c69303", size = 502464, upload-time = "2026-09-02T14:59:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/ca/04/d99d0b32c9265d0890b071fc062ae76c0dedc94a8071b9c36c8b0962d6a2/azure_cosmos-4.17.1-py3-none-any.whl", hash = "sha256:a33b796d055d1923b6dc7b6195aa1cfb5ae786dbc38771bb4ad702b0e5ec18db", size = 505766, upload-time = "2026-09-16T22:21:17.575Z" }, ] [[package]] @@ -1321,7 +1325,7 @@ wheels = [ [[package]] name = "azure-storage-blob" -version = "12.30.1" +version = "12.30.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -1329,9 +1333,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/7e/834d7bfcf999ab89d1bd3a5d235ece6824686da2f3d315e2162c613fe43d/azure_storage_blob-12.30.1.tar.gz", hash = "sha256:7a24f978c51d56a0375beebffcbe8453e59ae390d2695705848edc75083e4184", size = 624787, upload-time = "2026-08-27T19:12:54.967Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/f0/3fb4ec2d4b3798e3bb8fefb1096d5728f2501c2ed52e0cb729a2c956cf00/azure_storage_blob-12.30.3.tar.gz", hash = "sha256:31760221ceff36b7ceed741ba0093d08ec857911b92294dce3e4efe8fa40245e", size = 625235, upload-time = "2026-09-22T21:50:04.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/90/f06915ccf78a6d965901aae093bd7e88e486c52e0773202656fb29c2304a/azure_storage_blob-12.30.1-py3-none-any.whl", hash = "sha256:7dc09c37f4f58508e20532b4b4c178f4763f41b01e0b9063835b994fd9d2a7b3", size = 438131, upload-time = "2026-08-27T19:12:56.796Z" }, + { url = "https://files.pythonhosted.org/packages/ff/20/6530bbc13fea536a1f2c0194034ae3f6880fbc0c1e7c229e3569450f7aef/azure_storage_blob-12.30.3-py3-none-any.whl", hash = "sha256:80729a5397e5f0a63dbbdff74da675c5db1eacaf25fd5db0074b7ba6c17e2498", size = 438390, upload-time = "2026-09-22T21:50:06.395Z" }, ] [[package]] @@ -1469,30 +1473,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.89" +version = "1.43.103" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/26/48b3da85526a72a02df55e564481fc348e93699c15f0f502681b12ac2c8a/boto3-1.43.89.tar.gz", hash = "sha256:c28abbe472e9b7cad08807356311aeec51bde5218c18489da827045d2267bfd9", size = 112702, upload-time = "2026-09-04T19:24:57.143Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/59/012898d78087105e9c20fe31605d3f1999921745e1890ee0f0d993846e2e/boto3-1.43.103.tar.gz", hash = "sha256:524821052527f6446d249bf710847b032d9b12135e346c9751777a0a2811cf04", size = 112687, upload-time = "2026-09-25T19:26:30.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/12/e1b5cb4a00a9bfd72cf2d3f982c5826757aacdfc90aa4bd61902dcc94856/boto3-1.43.89-py3-none-any.whl", hash = "sha256:fe4190afe63eb562b6ba6a3911cf4427473b35fa047adde093bf696d3ae09fc0", size = 140028, upload-time = "2026-09-04T19:24:55.929Z" }, + { url = "https://files.pythonhosted.org/packages/f2/66/6eb2e92c57b30e54c87fe14c8cd71de3aed9184f3cec95c39132a3fcc61f/boto3-1.43.103-py3-none-any.whl", hash = "sha256:574796187eb4e4fcaaf5c1b88c1724a1ba5a95bc27aeae5573676178666acc71", size = 140044, upload-time = "2026-09-25T19:26:29.085Z" }, ] [[package]] name = "botocore" -version = "1.43.89" +version = "1.43.103" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/06/f63fb1befdf77af18539fb24ea01f2da0f13965ed5de091061708ac96416/botocore-1.43.89.tar.gz", hash = "sha256:f0574942970742657b0e0716cf08c2dfe6bef8e6de5fbb7081c3424e262b4cca", size = 16074206, upload-time = "2026-09-04T19:24:52.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/83/acbee6f2e3b1f02de935fef0c48a387511bf979149dc6b44466aaafba6a5/botocore-1.43.103.tar.gz", hash = "sha256:8c7f220e09f3b7ec99c59c716aa66cf6f5b2b57fd8731544054f3778aa744c6a", size = 16243609, upload-time = "2026-09-25T19:26:25.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/9d/96f9dee6d12eedf1c2b4264eefd59c7ac8cac10daadb9a7bfccc9ee881c6/botocore-1.43.89-py3-none-any.whl", hash = "sha256:d7211220c815427fe71225acc6909e4ab5dfab3b03770e72fd16cf9eb86b3d1a", size = 15768272, upload-time = "2026-09-04T19:24:49.769Z" }, + { url = "https://files.pythonhosted.org/packages/8c/47/790ba88ec849d1e07b5866458b67e79b291164f3de7f23429b4dcb7e2ced/botocore-1.43.103-py3-none-any.whl", hash = "sha256:6a6a561a6de43845ae42bc7ff7e2c798742bfa277f5215398f9c15ed9b1bd67c", size = 15938608, upload-time = "2026-09-25T19:26:23.162Z" }, ] [[package]] @@ -1509,7 +1513,7 @@ wheels = [ [[package]] name = "c7n" -version = "0.9.52" +version = "0.9.53" source = { editable = "." } dependencies = [ { name = "argcomplete" }, @@ -1598,7 +1602,7 @@ lint = [ [[package]] name = "c7n-awscc" -version = "0.1.37" +version = "0.1.38" source = { editable = "tools/c7n_awscc" } dependencies = [ { name = "boto3" }, @@ -1623,7 +1627,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-azure" -version = "0.7.51" +version = "0.7.52" source = { editable = "tools/c7n_azure" } dependencies = [ { name = "adal" }, @@ -1754,7 +1758,7 @@ requires-dist = [ { name = "azure-mgmt-dataprotection", specifier = ">=1.4.0,<2.0.0" }, { name = "azure-mgmt-desktopvirtualization", specifier = ">=1.0.0,<2.0.0" }, { name = "azure-mgmt-dns", specifier = "==8.0.0b1" }, - { name = "azure-mgmt-eventgrid", specifier = ">=10.3.0b4,<11.0.0" }, + { name = "azure-mgmt-eventgrid", specifier = ">=10.5.0b2,<11.0.0" }, { name = "azure-mgmt-eventhub", specifier = ">=11.0.0,<12.0.0" }, { name = "azure-mgmt-frontdoor", specifier = ">=1.0.0,<2.0.0" }, { name = "azure-mgmt-hdinsight", specifier = ">=7.0.0,<8.0.0" }, @@ -1810,7 +1814,7 @@ dev = [ [[package]] name = "c7n-gcp" -version = "0.4.51" +version = "0.4.52" source = { editable = "tools/c7n_gcp" } dependencies = [ { name = "c7n" }, @@ -1851,7 +1855,7 @@ dev = [ [[package]] name = "c7n-kube" -version = "0.2.51" +version = "0.2.52" source = { editable = "tools/c7n_kube" } dependencies = [ { name = "c7n" }, @@ -1880,7 +1884,7 @@ dev = [ [[package]] name = "c7n-left" -version = "0.3.38" +version = "0.3.39" source = { editable = "tools/c7n_left" } dependencies = [ { name = "c7n" }, @@ -1913,7 +1917,7 @@ dev = [ [[package]] name = "c7n-mailer" -version = "0.6.51" +version = "0.6.52" source = { editable = "tools/c7n_mailer" } dependencies = [ { name = "boto3" }, @@ -1979,7 +1983,7 @@ dev = [ [[package]] name = "c7n-oci" -version = "0.1.26" +version = "0.1.27" source = { editable = "tools/c7n_oci" } dependencies = [ { name = "c7n" }, @@ -2004,7 +2008,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-openstack" -version = "0.1.42" +version = "0.1.43" source = { editable = "tools/c7n_openstack" } dependencies = [ { name = "c7n" }, @@ -2031,7 +2035,7 @@ dev = [ [[package]] name = "c7n-org" -version = "0.6.51" +version = "0.6.52" source = { editable = "tools/c7n_org" } dependencies = [ { name = "c7n" }, @@ -2054,14 +2058,14 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-policystream" -version = "0.4.51" +version = "0.4.52" source = { editable = "tools/c7n_policystream" } dependencies = [ { name = "boto3" }, { name = "c7n" }, { name = "click" }, { name = "pygit2", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pygit2", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pygit2", version = "1.20.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, { name = "requests" }, ] @@ -2090,7 +2094,7 @@ dev = [ [[package]] name = "c7n-sphinxext" -version = "1.1.51" +version = "1.1.52" source = { editable = "tools/c7n_sphinxext" } dependencies = [ { name = "c7n" }, @@ -2117,7 +2121,7 @@ requires-dist = [ [[package]] name = "c7n-tencentcloud" -version = "0.1.34" +version = "0.1.35" source = { editable = "tools/c7n_tencentcloud" } dependencies = [ { name = "c7n" }, @@ -2484,130 +2488,130 @@ wheels = [ [[package]] name = "coverage" -version = "7.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/fc/fbd92ecbe5efbe68cf9e708d858570ebd33f0761600358948882f1a2a96b/coverage-7.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36aed4951aedf04cbe9465e76f8e71219980a52b73d07afe69746cba6ba7b97a", size = 222890, upload-time = "2026-08-28T21:50:37.361Z" }, - { url = "https://files.pythonhosted.org/packages/c6/54/16d2a7602ddf169353344e135541731cc24c7c3ef0001b0302f4d1a3de1e/coverage-7.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb953835dbfa6d641ac3943e0986bc680f8abbdc2985af15b46c54985347146a", size = 223415, upload-time = "2026-08-28T21:50:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/52/a1/15a36a42b35f6dd66214701c4f797856a9e42d12d85f441101eeb349404c/coverage-7.16.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97051c4903689b1afedc2a354d6118223051e03588078b53048603bda9014577", size = 250146, upload-time = "2026-08-28T21:50:42.509Z" }, - { url = "https://files.pythonhosted.org/packages/8a/bc/677f6363054d2de71fd0ca2071a796e3cf7cf82f8046933b34f2f91eb031/coverage-7.16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:770d4244c423dcafb5c31db393f429fe952b1bba23bbff7cc3886f8133769ba5", size = 251977, upload-time = "2026-08-28T21:50:44.137Z" }, - { url = "https://files.pythonhosted.org/packages/40/fc/9be462bb9257d84e3cc7517dc118c364db0eabdd6cb42272bb8667abedce/coverage-7.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26e7de0cb87960c6c9b5cad760068dab767b2b49a3b9376e1992c1e2691a015e", size = 253840, upload-time = "2026-08-28T21:50:45.822Z" }, - { url = "https://files.pythonhosted.org/packages/24/cd/dc003310b876c793c88f8dcf64ca52db244e4b6f96772251b1814fbb0653/coverage-7.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c2c45ee1853668f0ea1a0ddff396421c9dc5ad25a56bfb94a895970c2d8e7c2", size = 255756, upload-time = "2026-08-28T21:50:47.351Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f2/7a0a3c57e488b24d3ed560fc0e449c3e94fe8da0b59ef8dd00b2581c813a/coverage-7.16.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6b2b9599e7513b0a9c5bf0357f9f8deaa4c2c821025b0693d420e6602748981", size = 250811, upload-time = "2026-08-28T21:50:48.974Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1d/fd0cffd02a34eec7b92cfa4089a9d82e95390facebd56e5603de780b4727/coverage-7.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fde65e0ea945920265dfe4a2108fc45eee2e2ea3d9c3073af6373ff9836aa71", size = 251882, upload-time = "2026-08-28T21:50:50.516Z" }, - { url = "https://files.pythonhosted.org/packages/97/b1/82159b5ab545209764eb3eb0f06e315e9a2fd975a72bbf6da48d5deb6e76/coverage-7.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78103e79f9378cb0e43ddaa728629a373c070df903c5dfa98b63ba2cfb4e8c42", size = 249886, upload-time = "2026-08-28T21:50:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d3/91/ad689bb219fc78eaea8ff2b2212fa6202d4b716a65a533387183bb7a78ad/coverage-7.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e40e323711b485592354069b1c027ef879cc2d11657eac09a6e5ad0b49ab7406", size = 253698, upload-time = "2026-08-28T21:50:53.832Z" }, - { url = "https://files.pythonhosted.org/packages/4a/57/8eef40eb196d3fa1c0bbd99466119a40f70e5f2242eaa8f8852f98f4d985/coverage-7.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c94ef980f7b94d9dab9dac076d44ca706654cd51bad19734e029084adf528c8e", size = 250158, upload-time = "2026-08-28T21:50:55.64Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8b/0c0240eb6917c81ea21180bbc098bc01c624868bb917e9a10fffed23b970/coverage-7.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b37ad5cbb77776f446e1b55b461eec2eef5c3e7130c72dc0e1447c3a9da2d199", size = 250759, upload-time = "2026-08-28T21:50:57.272Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/6b4986aedfaa69a4607f24cf127527dbab4d57bfbeef4fffa126a5dbad6a/coverage-7.16.0-cp310-cp310-win32.whl", hash = "sha256:9421dde689e68d9fd2b6cd7d8c4498e79b5431467b6298517e3f3e60fdbe80a7", size = 224939, upload-time = "2026-08-28T21:50:58.871Z" }, - { url = "https://files.pythonhosted.org/packages/90/b4/43cdf444ace1c30c15446b143fa79d4bdd756cfdfce4c63f10c7697ba957/coverage-7.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:81d63b68b26304e3668edb103311c17fe13c2ed1c7fe973309819f27bf61c5b8", size = 225568, upload-time = "2026-08-28T21:51:00.389Z" }, - { url = "https://files.pythonhosted.org/packages/53/d2/c76bf165ff01664ca8b1ca7f2b2b5f311353d3959dbac1187dd21c6cc7f8/coverage-7.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:22d8802827404be32f5a4d6ddc037f6fa0074b7d06702c0224cb598def8b665d", size = 223019, upload-time = "2026-08-28T21:51:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/16/7d/a47cebf71cb789b6e25de07035d350bff110d02f9c28bf32f92b4c818874/coverage-7.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a739bf08cdca0fad51b73322e4fade0102dd87794e278450b5ee87ef827954db", size = 223524, upload-time = "2026-08-28T21:51:03.632Z" }, - { url = "https://files.pythonhosted.org/packages/51/b3/42e46d7e247ba33758156a0cc88dc64715f7e7b04640fbe430c4da437ab1/coverage-7.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f99d12f8234c00b88b8077fedf288b25c77f746de312053b7db90fa756ecbdb3", size = 253934, upload-time = "2026-08-28T21:51:05.365Z" }, - { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4", size = 255846, upload-time = "2026-08-28T21:51:06.922Z" }, - { url = "https://files.pythonhosted.org/packages/c5/50/38e5d8cf45af5db7419e9580bba4017113f8f1e2697cb6c52213bf7e7e40/coverage-7.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55957d350452017f523b9b03ffac078f9a214e23c04a3d0a674569203550c719", size = 257953, upload-time = "2026-08-28T21:51:08.51Z" }, - { url = "https://files.pythonhosted.org/packages/9b/bb/2f44b99723d0306095dacdf90f994631e299ff8f087a384b42ecc2d1ccb9/coverage-7.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b670bd5fa93d9b6855b2837217b45a90863118e2de5e9e033aebd46d07cd08d3", size = 259915, upload-time = "2026-08-28T21:51:10.155Z" }, - { url = "https://files.pythonhosted.org/packages/ab/7d/3f1c312944d88b2d3cae8af72007c15dcf5f92bda6da6d433c2d5f050ee7/coverage-7.16.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe5aa402d02318db2f41e471320b2ecca6085b8f595a034c037085732e49c04a", size = 254028, upload-time = "2026-08-28T21:51:11.845Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f6/52a7e26baeeca7f3114b15da5e840bebcfe6491eb234f6922d33c79ee8fc/coverage-7.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fddd26ed9a2527a7e23f7e4c1fd0734c4a5b45f77b261da1c536b20a7d2e6f0c", size = 255648, upload-time = "2026-08-28T21:51:13.614Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d1/0673e78d9ca29d56f663623791338647753c673f0bc964e860086da07bce/coverage-7.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b2af58ecdcec37fe633d4865fccbc8c00d8aa3b31c099bcacb2720c9a0be6ab9", size = 253708, upload-time = "2026-08-28T21:51:15.19Z" }, - { url = "https://files.pythonhosted.org/packages/6c/23/b74c87828369059415b20884b6f48260f049bff750d6eb454be8554732ab/coverage-7.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a3cd34b9025d62180ce2b5dae8a985bfa6cb8c05ecd57fd34ffc1ff751b5a74d", size = 257479, upload-time = "2026-08-28T21:51:16.988Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b4/09e172472c45a956e226dddf82d449f245764208b7cea47b32a73df955a3/coverage-7.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ebaf39dd13f8af65fe5f0316b81046228ef4d91d3c3766192b418753649896d6", size = 253428, upload-time = "2026-08-28T21:51:18.803Z" }, - { url = "https://files.pythonhosted.org/packages/62/22/e378e4f7ffa290ea4775b34e319fa182640bba650a2c6781af791b66b79a/coverage-7.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5dad64d9c17cb1983adef07998e6e2e1cf870a156f1ea80f81ce1970f4c545ce", size = 254337, upload-time = "2026-08-28T21:51:20.785Z" }, - { url = "https://files.pythonhosted.org/packages/51/6f/9a6ca653d86e46c3383a905f726a28bcf7bb2528088794d30a53687b381c/coverage-7.16.0-cp311-cp311-win32.whl", hash = "sha256:38b8e1e73750b8965d1154ed733f5303acd4e24ee2d5ee872bb1bfab744a31ce", size = 225103, upload-time = "2026-08-28T21:51:22.685Z" }, - { url = "https://files.pythonhosted.org/packages/08/0c/6d4627be89ac02f579d88806875a5d6e328c59d7d79c594643c7a4460ef6/coverage-7.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc12e5e32acdd62fe5895939695579560639853219288519685c75b7e968d63a", size = 225577, upload-time = "2026-08-28T21:51:24.334Z" }, - { url = "https://files.pythonhosted.org/packages/f2/3d/d7be38564d00a17775426685776b4bf18e8a6048a085eccf65d75eb0fa5a/coverage-7.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:17fc3628f99812fec24f40092af34c1c73274d331babab3d1d768a75de650cf7", size = 225126, upload-time = "2026-08-28T21:51:26.101Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, - { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, - { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, - { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, - { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, - { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, - { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, - { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, - { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, - { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, - { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, - { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, - { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, - { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, - { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, - { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, - { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/850675f262391b322c4c988b6cdc32cdc6629288f0fb158687b587a393a8/coverage-7.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:54b7fba6a74d010de34319a0419d5b65af8c00f539ad0b6f39fc6f342ab99697", size = 223258, upload-time = "2026-08-28T21:52:23.558Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/4f54c6d47c80d1cc58ef8fe6b74e6eb50f9e2c0f6e2de6cf38dbca2937b8/coverage-7.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4ff0b3dd52208d2b30903022d5087f82000507b504753dfeee83e4f32d6883", size = 223587, upload-time = "2026-08-28T21:52:25.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, - { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, - { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, - { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, - { url = "https://files.pythonhosted.org/packages/d1/45/d0bd410e78cfbf768acc8099b335e1d5c0d5c26103c796d2bebdee001715/coverage-7.16.0-cp314-cp314-win32.whl", hash = "sha256:136988df5bc5a48795d9c42c75c4bbda5d9a78e750a080c1233010edff93a1af", size = 225424, upload-time = "2026-08-28T21:52:47.658Z" }, - { url = "https://files.pythonhosted.org/packages/17/78/1ce6ce4646822e9308dcdb1942eaf31bfd7da43247b8886338b0d6fe3767/coverage-7.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce2ba5e9f1842fe09165825abfb3bc6b527c71a27bc2eb3a10f2284ced64506d", size = 225918, upload-time = "2026-08-28T21:52:49.692Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cd/e1323fe3a7dfcdd709451a43fe708ca1dfd36a7fc07b34eb7bd1dfdfb52d/coverage-7.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a89d07e48d9baead9a15599923a02f62c6df6c3d85aa84ef34be3c9fd6aeb91f", size = 225344, upload-time = "2026-08-28T21:52:51.665Z" }, - { url = "https://files.pythonhosted.org/packages/39/fb/1c15460d4cf915f09ae3ad3862fef4f901838991c5641b0cec545050d810/coverage-7.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6e2854b62601c89a63814ad5def3b90d99c6724cc4cb977f75b725e5fca4b1e3", size = 223986, upload-time = "2026-08-28T21:52:53.572Z" }, - { url = "https://files.pythonhosted.org/packages/9f/73/347d2d0009ac211f79ee2a2364fd2aa19d6b9628dc22ed13a9b9386097ab/coverage-7.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f093faf23df888518d273be6da65f0ec5a25b5d8b670231e4d87de07361042e7", size = 224254, upload-time = "2026-08-28T21:52:55.59Z" }, - { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, - { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, - { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ec/84b4af5cd4ad498477b3bfb2217e47b048da919451053790efda66f7383c/coverage-7.16.0-cp314-cp314t-win32.whl", hash = "sha256:058631257350b31784ed43ceb808298b6f074edf4ebca4c7ce5082e6bf873a61", size = 225736, upload-time = "2026-08-28T21:53:18.632Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/50fc0e6c675c3ef14895a74bab2d6120cb5d6f4b562a3d3f5046797758dc/coverage-7.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed35097438dfa980c1ec75bc83edf8acbe7a374d7007e571957a257fbd0e2fb3", size = 226570, upload-time = "2026-08-28T21:53:20.754Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/9effce7bcd3c6eeb4da3561905837509e582dcdde7a7f07d6ef2c8512f76/coverage-7.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0466f4a5c0370461b7d8c7eb259d7d1db0b5756f13d66230b04d22a1d380ee11", size = 225879, upload-time = "2026-08-28T21:53:22.747Z" }, - { url = "https://files.pythonhosted.org/packages/4a/2c/318e4379106bc8047ba235e3732ddc87d1b393ac3db9776f5405ff14f322/coverage-7.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:80d7d5d744a041f08637df743ac086204ec5acbcd8432a42b00b49e607358024", size = 223257, upload-time = "2026-08-28T21:53:25.376Z" }, - { url = "https://files.pythonhosted.org/packages/81/4d/a5c54d9144e9db6505749758ba50a28be624148873751728a59cbb72d27a/coverage-7.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c5feffce90c3d602e149de1c477578efc34dee5f069f9764cc15808ce01ee15c", size = 223596, upload-time = "2026-08-28T21:53:27.461Z" }, - { url = "https://files.pythonhosted.org/packages/bc/97/38e93a10899c9315964c0a4e729b3e5867f8f46e977808f9c6fbda52525a/coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633", size = 254699, upload-time = "2026-08-28T21:53:29.715Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/acddda030b4630f68167f3daa94b41d22071847822a70d8178d43dcf678e/coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b", size = 257614, upload-time = "2026-08-28T21:53:31.948Z" }, - { url = "https://files.pythonhosted.org/packages/15/7e/225b182497c1ce6d3f0d76a3074a4dbc9f272300e92bb100df53b03de0aa/coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f", size = 259236, upload-time = "2026-08-28T21:53:34.291Z" }, - { url = "https://files.pythonhosted.org/packages/2e/19/76641ddc50cb2410ebbd0ed7fe1052614d0e5612e802a2817521adb9febb/coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68", size = 261433, upload-time = "2026-08-28T21:53:36.401Z" }, - { url = "https://files.pythonhosted.org/packages/12/9e/5f89de8b7c2017f36b68b4e4a25940723a748b21474820bf61e8bce0891c/coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be", size = 255182, upload-time = "2026-08-28T21:53:38.496Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c1/ce94b2ec502e79775efb5efa22c741ebb0bd2be10bdd29650825ff57bdcb/coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a", size = 257329, upload-time = "2026-08-28T21:53:40.87Z" }, - { url = "https://files.pythonhosted.org/packages/86/8d/3f5374df3a6ca19ee5f98a6bd21dbb05f1e9d399bd9978e9821d260eab5e/coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae", size = 255210, upload-time = "2026-08-28T21:53:43.393Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b8/1bc5751496d0be6fd9dde8ca547d9a8a9f07847856aba3f3ae5ac594cd81/coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee", size = 259442, upload-time = "2026-08-28T21:53:45.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/dc/8aca78e47e1e6fcc761cd28a20daf4a84bd847a7369e2701a93ccfc3d1fd/coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f", size = 254618, upload-time = "2026-08-28T21:53:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/fd/787842cdf6ce16ac5c1bd8a26549bab3b3f27b02500075bc540dc7853bca/coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73", size = 256541, upload-time = "2026-08-28T21:53:50.805Z" }, - { url = "https://files.pythonhosted.org/packages/ef/79/8df302cbef373dd1f3401044cdb94dfc74517e5af2af27b4d0e721557e0e/coverage-7.16.0-cp315-cp315-win32.whl", hash = "sha256:5205baea687133613dced668a3d0168ea1479349615bfc255849a7944988c889", size = 225429, upload-time = "2026-08-28T21:53:53.177Z" }, - { url = "https://files.pythonhosted.org/packages/85/87/5bad7ac45f76b3728ca211028ee561c2ede3ba44da401129e28bb8737291/coverage-7.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:4fcb5f07a9b7083bfb715115d27ce263ba2b5b89dddeee536b295ba0e3c2c627", size = 225903, upload-time = "2026-08-28T21:53:55.535Z" }, - { url = "https://files.pythonhosted.org/packages/cc/ea/67d84b11caf240f059ec313f616d82212df5004e8bc85802c1edfc50bb3d/coverage-7.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:d568a8adcec0eda42ec23e5e65dfb8c184fc255120f9e99b484f7c869d923fb9", size = 225334, upload-time = "2026-08-28T21:53:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/65/21/a88349cce3ff720729b754916ac47e2e3646a8137552e4fa7cdd5967cc7f/coverage-7.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3e8037e8213adf882e9d7eedd2c5c557933ab0b9632c42d98fe98ec9bcdb4025", size = 223980, upload-time = "2026-08-28T21:54:00.082Z" }, - { url = "https://files.pythonhosted.org/packages/fd/02/4d54abf3e6a4d8b7675921b20e91163b1064a5a9dbefebb71c05065dd136/coverage-7.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:289f2ed4d56eebf029b649e7dfc3c1153b111962a75e294cdd8e4a1598a04cc3", size = 224276, upload-time = "2026-08-28T21:54:02.381Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/10dbc96d95d20b9b041045d293480bd49e536180e93af62dd7662376284d/coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a", size = 265135, upload-time = "2026-08-28T21:54:04.558Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/6b326544afd1a8aef3a495bbae109a7ab5baf23e04a2741d8d64e2df2ba2/coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178", size = 268216, upload-time = "2026-08-28T21:54:06.97Z" }, - { url = "https://files.pythonhosted.org/packages/54/34/1dc8265f3ed990690e24d5f31ff79bc9fb9b25d54f9f89bebad5a6a8b7a1/coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2", size = 270772, upload-time = "2026-08-28T21:54:09.234Z" }, - { url = "https://files.pythonhosted.org/packages/66/a7/3a8463713a402b44044ec832f4a76e442ce4b3a207804303f4d1dc1a9bb4/coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0", size = 271752, upload-time = "2026-08-28T21:54:11.701Z" }, - { url = "https://files.pythonhosted.org/packages/25/3b/dd5e795cfbe1842f69899189089ae289a96d6a68de312960ea668542e33c/coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b", size = 265589, upload-time = "2026-08-28T21:54:14.12Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b6/fd90636cbd95cb018312f6ca1ca2bbd70fbe8e4ee6f3992fc36a4230364e/coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0", size = 268596, upload-time = "2026-08-28T21:54:16.303Z" }, - { url = "https://files.pythonhosted.org/packages/91/10/ef2d59264f3b3b358cc5885ca375e6cdbda7c195e78304d5aae800a72d9d/coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e", size = 265072, upload-time = "2026-08-28T21:54:18.597Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5b/400891c364c0170408d172501b340b18611800f4c42d8fbb16f9f5497c24/coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54", size = 269768, upload-time = "2026-08-28T21:54:20.985Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/9792c80271df04d287d21ed5d662fd8fa58b1737888d817679b1ce5d2fab/coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb", size = 265211, upload-time = "2026-08-28T21:54:23.344Z" }, - { url = "https://files.pythonhosted.org/packages/81/67/5b8f827cfa6616e6bd7ba9397acfe7e3c4fd5b9fca4125511d5089f55d5a/coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897", size = 267170, upload-time = "2026-08-28T21:54:25.85Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ee/c135d2d2cb617d744bc3e13c922f2fae66964494176ddef225dc4656bd2c/coverage-7.16.0-cp315-cp315t-win32.whl", hash = "sha256:27461af9f3ed7d2cf2411eb083784f87055ebf42211789ae3a216c48609bc743", size = 225731, upload-time = "2026-08-28T21:54:28.151Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4d/dc3d53eadf155916e183bf5dfacbfc4aa5bfb7f13b7da11c01caa7a05cbc/coverage-7.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:c5612cc20ca76abc883e50269af47c1494b42958bb63dbb9aa79729a1ab5f7d3", size = 226562, upload-time = "2026-08-28T21:54:30.42Z" }, - { url = "https://files.pythonhosted.org/packages/2f/00/ac9da1a60a4e84c3ad0f7db4723fd327154a8f9add210c0dcd2db3ec5156/coverage-7.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:2ddaa9e2af4760a329d80008b7a3b4762fbb0dbcb169199360f9a5179c32f2dc", size = 225872, upload-time = "2026-08-28T21:54:32.806Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/62/a8b4dd53308a4b1571e6bdd77ee562c9b1c1742db9886c0aef2f69463067/coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853", size = 223178, upload-time = "2026-09-13T19:08:21.805Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/b9325b8d486a5c05fd4a9eb1cd9c9f697c361ddeda0e73567d5c2d8959f6/coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097", size = 223701, upload-time = "2026-09-13T19:08:23.391Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ee/3ab3ac6e9e7165a623695b73a83d1bfbdf5069681a68a012cb5abb096060/coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a", size = 250434, upload-time = "2026-09-13T19:08:24.948Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/31ffc231e9879876f611cf1ac7622cf092d76762c07ca64dce47b29a200b/coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881", size = 252264, upload-time = "2026-09-13T19:08:26.734Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f4/4fb4f6b16de07dbae3d3a111ae28d32ad657ce6ff4d364c49c10962daad5/coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43", size = 254127, upload-time = "2026-09-13T19:08:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/048e88eb610f44b7542dac10fa45bb2ffad5fbfcdab0f03289a843da934b/coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b", size = 256043, upload-time = "2026-09-13T19:08:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/97/59/d27b113c80b04c7fc64c69457132648e1d287b83a1de41189a504b50b52b/coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec", size = 251092, upload-time = "2026-09-13T19:08:31.57Z" }, + { url = "https://files.pythonhosted.org/packages/45/42/b7eafaceff08dc1dcbfc924fa9233031fd00c8b8287cc4a1a0dcdedcb0f9/coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9", size = 252170, upload-time = "2026-09-13T19:08:33.191Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/57dcc5360aa6d8d24370c12fe855f6ebf5fbf2718c154fb9fdec7161de07/coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38", size = 250173, upload-time = "2026-09-13T19:08:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/379a8c7ac0f3063769dc43fd2475e9fc9b2722cb88eabe71f4d862660c93/coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a", size = 253986, upload-time = "2026-09-13T19:08:36.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/25/70218d2a6077730843b75ee2525fa64a750283d71cc4c063dc0b75ba6979/coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594", size = 250444, upload-time = "2026-09-13T19:08:38.434Z" }, + { url = "https://files.pythonhosted.org/packages/58/ac/6c41c441baa6c8a1fed5615e4e1e7d0ca649b33b472ee28591a92d70a1ff/coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773", size = 251046, upload-time = "2026-09-13T19:08:40.133Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/01166d6f8aa42acd148b4c2c88173d60eefdd0270ea2bcbba79be35bc193/coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc", size = 225242, upload-time = "2026-09-13T19:08:41.672Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/a76060478919becf0fbea2df2053a6e1a7195f11edf8dfd51c58c1636b43/coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90", size = 225868, upload-time = "2026-09-13T19:08:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, + { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, + { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/ae/16/e11addf5322d98e86307da9e00c94644b97cb72c534c74cda89705bebeef/coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743", size = 223544, upload-time = "2026-09-13T19:11:09.108Z" }, + { url = "https://files.pythonhosted.org/packages/92/d9/7ccd6484f615961d94b09c75904f87d86375109596069ae3a495a205dbbe/coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e", size = 223882, upload-time = "2026-09-13T19:11:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/59/37/2fd78152a557df4a7e4ad78d6851ede90c211838d526e4916c6016f45144/coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae", size = 254987, upload-time = "2026-09-13T19:11:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/58e2f9b8b13cc422aa7f474a498945ec7f446ef473b5b6bae18d2981f2a9/coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370", size = 257902, upload-time = "2026-09-13T19:11:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/63f8f6a67c03b86455c726be273b2aa5b57fd1122f22350d141bbb142e91/coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae", size = 259522, upload-time = "2026-09-13T19:11:18.206Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/1f2261da7483e345fe55de11e41ffc16a543511e37c0912134c0dccc4f6c/coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de", size = 261723, upload-time = "2026-09-13T19:11:20.628Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8a/7eb1a361e044b7293f49c921a974b532627ed37f3bef5e7e9f58bcd0d5a2/coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1", size = 255462, upload-time = "2026-09-13T19:11:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/67/66/6d94f7ea87e99c519def5cfa5d4a1ab20247d6b403eb6e6b5b63d6aa5985/coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c", size = 257617, upload-time = "2026-09-13T19:11:24.933Z" }, + { url = "https://files.pythonhosted.org/packages/3d/52/c3de0a3868589a1463a4f1cb0222eca0e5fa9f91da865ccf20d2e19ec19c/coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6", size = 255495, upload-time = "2026-09-13T19:11:27.158Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a3/ea10e75f20fc1e7be10b166a494826a965d19b1dc9c8a81c511a01153311/coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f", size = 259728, upload-time = "2026-09-13T19:11:29.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/96/9d8020d97de46f0390739beda196322586cdd87e1fc357223e6a28bc4135/coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df", size = 254904, upload-time = "2026-09-13T19:11:31.86Z" }, + { url = "https://files.pythonhosted.org/packages/5b/21/2ff8867d4560f4f639a82d8fcf13c27eea88a96193daba096027c48bb929/coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040", size = 256828, upload-time = "2026-09-13T19:11:34.078Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4b/feacad51cb5163e3baa13281f8a3098f15d7934cc1f28cfe656557eb4e46/coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827", size = 225722, upload-time = "2026-09-13T19:11:36.314Z" }, + { url = "https://files.pythonhosted.org/packages/39/34/66bbc6ffd3c51fa67604c566920e772c5e3baa57f88dcf7b109c7f18b2cb/coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d", size = 226195, upload-time = "2026-09-13T19:11:38.493Z" }, + { url = "https://files.pythonhosted.org/packages/f7/96/4bad920d4caed127c37c136a64b0730fa495517d2eb58809e0d81e6e7dff/coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855", size = 225625, upload-time = "2026-09-13T19:11:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2b/a88c7906eeb7da4394e932060d7008e153b3a2cd8d11782d82faa03bb7a3/coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6", size = 224271, upload-time = "2026-09-13T19:11:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1f/98db59c595680d16f553890deac6d72610e04f192c1964ec9b69cbe790ab/coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d", size = 224564, upload-time = "2026-09-13T19:11:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/699e236d4cb97ec282b0655afc85acf12349e71a02fff0f1a7d0fb643079/coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6", size = 265423, upload-time = "2026-09-13T19:11:47.666Z" }, + { url = "https://files.pythonhosted.org/packages/96/1b/6eaa21912863b3e3bf23cfe605fb62929bb1a41dc33d2bfa8e92000232bd/coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1", size = 268503, upload-time = "2026-09-13T19:11:50.12Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ac/4eb46bffa98c28a80559a58c12f17e19308d52b31174af1bd49decf76606/coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1", size = 271059, upload-time = "2026-09-13T19:11:52.363Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d4/17a51ef1f6a084c9abbead522cfbee0fe9d29a593128781e724b7a0795b3/coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b", size = 272039, upload-time = "2026-09-13T19:11:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/9fdeba8b8aa0848030f45f869ddedc306795a8c6e53e901b8553afbf5415/coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de", size = 265869, upload-time = "2026-09-13T19:11:57.423Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ab3eb506b2e4dd278440fbbbc7ed18424a86d8a981db0b90fdba66f5d34d/coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef", size = 268883, upload-time = "2026-09-13T19:11:59.71Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0c/f4edefdce33f01954a74237df5ee83ccef5dcd1165e04abf0adf47543fca/coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876", size = 265359, upload-time = "2026-09-13T19:12:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/df/86/ce5ed885fc7ce2adc9a5971134ace257d0f84f19c3d117da89c179b9662e/coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8", size = 270056, upload-time = "2026-09-13T19:12:04.46Z" }, + { url = "https://files.pythonhosted.org/packages/48/a1/5dbd5f95070cece25c6df9e6ea4ded2a87dc695ce82a49bcb2116452b342/coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220", size = 265498, upload-time = "2026-09-13T19:12:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ff/254b26f3300f87c7c53ac97da41ae75f0e2ac066aa964bc073c0f3750b38/coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0", size = 267459, upload-time = "2026-09-13T19:12:09.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/20/bf9958f8ddbbf6a2d39da3d4009f33e0fcf19c2d2fd99715c6494cdc8e8c/coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5", size = 226021, upload-time = "2026-09-13T19:12:11.933Z" }, + { url = "https://files.pythonhosted.org/packages/35/8f/12d46948704d9e437c8dca2718e5d54ecc2f85396b64b500c29d8dbe10db/coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db", size = 226853, upload-time = "2026-09-13T19:12:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7f/1ca5fd0601054fce5a3539375b997e67683646e2bf62f60c41e70b529fcc/coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0", size = 226163, upload-time = "2026-09-13T19:12:16.589Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] [package.optional-dependencies] @@ -2756,14 +2760,14 @@ wheels = [ [[package]] name = "datadog" -version = "0.53.0" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/4a/8c61a6bd82be61a7b2a1f9dd77104f52299aa81d377fe6a35f9477167361/datadog-0.53.0.tar.gz", hash = "sha256:9f69a18089c852e20edff7e55226a6f4ca9749e162cfa4ae88c949dd722ebef8", size = 378021, upload-time = "2026-07-24T15:11:56.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/01/0f7b044e0cd3f3d851948a612fc09d48cd28b436fe09dd357fbde8f134aa/datadog-0.54.0.tar.gz", hash = "sha256:62afcedb3e56b8ea6fff04aa807ebcbf9a6bd843bd1c7feab0a9035ce21b8ca2", size = 407291, upload-time = "2026-09-22T20:53:51.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/83/1fda660aef15fa7f80fc9ad0f056d45dbe8dd09b1774a9cd6c4cfc6cf24d/datadog-0.53.0-py2.py3-none-any.whl", hash = "sha256:e42467bad752c4739a232986e4ebdb8e4690134e901965557f2eaff0f40eb177", size = 138980, upload-time = "2026-07-24T15:11:55.221Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7f/d9538fded4be4f13b26e2848e74aafdf8d20d4bee94b966c443145c53cf8/datadog-0.54.0-py2.py3-none-any.whl", hash = "sha256:5a3d47154ed5ef223ca85533da3703691ba8707812db1a95f335094cdf884d4b", size = 152270, upload-time = "2026-09-22T20:53:49.677Z" }, ] [[package]] @@ -2829,11 +2833,11 @@ wheels = [ [[package]] name = "durationpy" -version = "0.10" +version = "0.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/5d/5f8571bd5dedc80863191621ac4be001f3f3dd8315d2ec078705dab7dec1/durationpy-0.11.tar.gz", hash = "sha256:181898e1ae282e288f0a2291829656bf1b6b3aadf30a97993b85db4943642905", size = 3582, upload-time = "2026-08-26T13:56:00.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c4/ebdf7837bc4ef6fd98cfb013c28855bb358467bf86c1af011bbc21e21df0/durationpy-0.11-py3-none-any.whl", hash = "sha256:a739fe2b8972c250ff72f8e2c488d18cf25f7b852f49ee76048775d5171df30c", size = 4133, upload-time = "2026-08-26T13:55:59.456Z" }, ] [[package]] @@ -2853,7 +2857,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2871,16 +2875,16 @@ wheels = [ [[package]] name = "fakeredis" -version = "2.37.1" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "redis" }, { name = "sortedcontainers" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/5e/fce3355d20d37dc18f7f4c422653ac69cabffa019afaaf607e26704fb829/fakeredis-2.37.1.tar.gz", hash = "sha256:9045851b0a9fe56312696aadc82435141aa43a193cab462d372c8fb583a7c087", size = 238182, upload-time = "2026-08-18T15:22:01.676Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/6b/ad7db311fea3a62f7e359720cc8c064f06f8f0006e65272f50d3a23d37f6/fakeredis-2.38.0.tar.gz", hash = "sha256:d2abfd24652f86501044499bf08c9d639db050f695eefc06b8b8b6f0bb24dbd6", size = 271116, upload-time = "2026-09-08T21:15:04.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/e0/fe1c760abd9e9cb5dfbce057b6582cb1ac36dfd8b5ce202e1445605c922d/fakeredis-2.37.1-py3-none-any.whl", hash = "sha256:f15c41be151c1e9206416dece764369a4dedb6b1341df3734c3c2c000e405508", size = 152738, upload-time = "2026-08-18T15:22:00.1Z" }, + { url = "https://files.pythonhosted.org/packages/ec/8e/9a80a8808e8a723aacb3a5d9df07890b46b363a056b372d3cd2e655259fe/fakeredis-2.38.0-py3-none-any.whl", hash = "sha256:d9fb0518c4eaa35f1f2c94df6b4a4c97ff3ca9f43d6cc8112317a9037d244301", size = 167360, upload-time = "2026-09-08T21:15:03.219Z" }, ] [[package]] @@ -3018,18 +3022,19 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.34.0" +version = "2.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/8d/cbdc715cdfb7acd7ccf1ce2869b103734c6a3cf124afd021f56c10c17522/google_api_core-2.39.0.tar.gz", hash = "sha256:824ee414a10adefefae33fc5e2ba28dc6f0f7089d011c9cc98ea9178e61299e9", size = 258568, upload-time = "2026-09-24T23:20:02.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/41b3cd83d472b63c694a01e876d3aaa7c2b2f61f29ec8a9275f47b057349/google_api_core-2.39.0-py3-none-any.whl", hash = "sha256:40a5d1c892bc1798f3b75ab10c665b85717979088519c714ca5774ee81d1e3c5", size = 214233, upload-time = "2026-09-24T23:19:36.419Z" }, ] [package.optional-dependencies] @@ -3056,28 +3061,28 @@ wheels = [ [[package]] name = "google-auth" -version = "2.57.1" +version = "2.58.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/aa/8055c583f8eb69dd4544f0d6a83e22d2f01ab38bd2ce848ea599c259a2d2/google_auth-2.58.1.tar.gz", hash = "sha256:1480461d8b2347e679af758f4003700ea79a59b603c9134e18bfa95e8004ee70", size = 380459, upload-time = "2026-09-24T21:52:00.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/cba0ac98e98e01849b19f7504eb3aeede741e72dfc31612e46d5f0dd981c/google_auth-2.58.1-py3-none-any.whl", hash = "sha256:66a63572d0ab629554768544df04018d79178c0bd7439ba31683ac576b79024d", size = 262483, upload-time = "2026-09-24T21:51:58.297Z" }, ] [[package]] name = "google-auth-httplib2" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/46/79983cb738f0eb14e6ab4f43457aa9652f8d46bc4376b178f676b68c5c37/google_auth_httplib2-0.4.1.tar.gz", hash = "sha256:125b1bb4fcfdd2d97f19b673c1f46f831603d0acaffe415c8a35dadb312552a1", size = 11158, upload-time = "2026-08-06T06:24:00.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/74/0c8177b73734dfbd89420c162ac8754257fa0f9007fb49569493d83a17db/google_auth_httplib2-0.4.2.tar.gz", hash = "sha256:916225a6367e613c9af44d83f41688a599d3f687777846b8b91bec65085ed1f1", size = 11211, upload-time = "2026-08-25T19:18:24.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/8a/96410d0c3e02b584d1e7f5d4000b2e1710855650b226cd0601f2b154940a/google_auth_httplib2-0.4.1-py3-none-any.whl", hash = "sha256:67088a8387dc3f66016a690634f1cb9b7a48a31406f8998e088efc64203e7a12", size = 9529, upload-time = "2026-08-06T06:22:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/e697a69f13bfd448d33a374231b89cbbb203dedfb4aae9d26126be6f6c72/google_auth_httplib2-0.4.2-py3-none-any.whl", hash = "sha256:4298dd6b1415972d0c464b7177e6a69a595e7aec5b972222ecdca342f6009647", size = 9529, upload-time = "2026-08-24T21:55:05.843Z" }, ] [[package]] @@ -3098,28 +3103,28 @@ wheels = [ [[package]] name = "google-cloud-audit-log" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/d1/fd23bd634ef7e2a1e27c6cf16f6dfa8b44c09d383044641cfef5d21ef9d9/google_cloud_audit_log-0.6.1.tar.gz", hash = "sha256:4aba72b45dc15344da1b2d8981320050fe39cfe80432f38e7318aa2e477c5bf2", size = 44688, upload-time = "2026-08-06T06:24:04.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/91/e2a4ed95e790564b807a61fd54d90aa6bf2d3a9474bc5197651acf0cad13/google_cloud_audit_log-0.6.2.tar.gz", hash = "sha256:ea5dc892b1858d345aef0c5563a728534fa875bd0388765391574d3c57fd8f0e", size = 41010, upload-time = "2026-08-25T19:18:29.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/db/af25d04b2e67c1dc49e00bf59e58a45962cf27f12f04b8dfca8753009971/google_cloud_audit_log-0.6.1-py3-none-any.whl", hash = "sha256:981b2ac69e69fe8ed0eb71998d8520a746d5db9bdfad0f088ab4b4628702824e", size = 44853, upload-time = "2026-08-06T06:22:56.144Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/f24fba2cb01ad41615d55335306642e3ddf3a5c3cdda6b44754d37bd650e/google_cloud_audit_log-0.6.2-py3-none-any.whl", hash = "sha256:1f19bdf2586cfdb72054eec769ede153a037988b73a7d3b6d266f672ab426c00", size = 40382, upload-time = "2026-08-24T21:55:11.849Z" }, ] [[package]] name = "google-cloud-core" -version = "2.6.1" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/c6/9d7d9ed6703eb35306ca7bf381fb66ba8d978c61a1b550a2e1730a4c4ce8/google_cloud_core-2.6.1.tar.gz", hash = "sha256:1e044b131f2ae097b92312fa195164b0aeb6dc6a88e00231e1210516314c420c", size = 36017, upload-time = "2026-08-06T06:24:18.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/54/790548b190cff9cf07225c811d2668eacd8f2cbf04114475cbe3e5738752/google_cloud_core-2.7.0.tar.gz", hash = "sha256:874aaf89765db87a9b911b7a2ca7c5068554868eed9e75c7766affe342a2913d", size = 36603, upload-time = "2026-08-25T19:18:43.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/4f/37960d5255988b218c40c9b5a2b731013684294a50735d1dd1ab4894e531/google_cloud_core-2.6.1-py3-none-any.whl", hash = "sha256:2682a8a4474a32f56292fb4bca7fa7e4fb0b4af958f6abfe4bca8d195747fd45", size = 29393, upload-time = "2026-08-06T06:23:11.405Z" }, + { url = "https://files.pythonhosted.org/packages/cb/64/904dbc9bee128e7b87eeb60da67c8fa4d1a8acdc9a45dd2fedca67ef184d/google_cloud_core-2.7.0-py3-none-any.whl", hash = "sha256:c18a250904cfdda021eb3ae8b8238c9f9ca272a4cbbfb5cba946b3fe3022eed1", size = 31046, upload-time = "2026-08-24T21:55:28.36Z" }, ] [[package]] @@ -3178,7 +3183,7 @@ wheels = [ [[package]] name = "google-cloud-storage" -version = "3.13.1" +version = "3.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -3188,68 +3193,71 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/73bb7512df1d1aad6ce3f9aed847cd40e0cd400ba4a85d86ab8eb412e9cc/google_cloud_storage-3.13.1.tar.gz", hash = "sha256:a80bf8cac2794808aa61c50c5f769ecbbe2d10331bacd0d69d30e59b14b346b2", size = 17341051, upload-time = "2026-08-06T06:24:42.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b9/de4f0a218485bdbac839ad337166997171471a29c7d28c03b41b37830379/google_cloud_storage-3.15.0.tar.gz", hash = "sha256:110041ee1cb566114015aa7c2492f1ff18c92af528588386663f4b41b6279989", size = 17350807, upload-time = "2026-09-24T23:20:16.709Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/6f/d69f0e185e08ddb58c323a0a935af2b492907b5de362bc08933b0a3b5644/google_cloud_storage-3.13.1-py3-none-any.whl", hash = "sha256:98208de6c21e85cecd3eb44551894efff33d98365500e178867d4305854a770a", size = 341486, upload-time = "2026-08-06T06:23:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e7/77c6358819e53f2cf5175e50b61edcf0af4f7fd1158e9703649659630b5c/google_cloud_storage-3.15.0-py3-none-any.whl", hash = "sha256:248905def997a300ad456c0696cf7fad04b8c38412c1103ebb2a60cc6341579f", size = 343392, upload-time = "2026-09-24T23:19:52.066Z" }, ] [[package]] name = "google-crc32c" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, - { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, - { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, - { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fa/25/9cb0c1c31c45b893eb8f11ae70b3f4309432d59b5acaebca5dbe791729a4/google_crc32c-1.9.0.tar.gz", hash = "sha256:7b8c84c3d159ab6817fe3f74e6e6cef099c3f95dcec3abc0d8afb1404642efbe", size = 14857, upload-time = "2026-09-24T21:39:32.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/7c/e89a13c971bcab4a0464ecc78f8dc162c5c7ec8986a54dd867fc093b2c6f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6b529a6a287104ec79d281c411685231200ce954a29c28ab8e5093cb6e130fb", size = 31959, upload-time = "2026-09-24T21:19:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/c6/06/510062c2acbdbf602d759b7b0086032c487126106bb25197b9da1ff1047f/google_crc32c-1.9.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:51cb4e23a38ad4f495f35f87c233ca3ea6b9c4559e7ac383cdef786fab0f7977", size = 31801, upload-time = "2026-09-24T21:22:24.222Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c6/53eaa12dc62625f4605760b09854a0814ef6afebdc0e611a38c7b15aa6d1/google_crc32c-1.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8535e75dfead304f30e9122b9ea2c0a570dbaa52c176a0a591540c7914c1e46d", size = 37203, upload-time = "2026-09-24T21:38:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/770c2713df471df73998f79758739da83e410ef576bfafd05e5e845959ff/google_crc32c-1.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:280f3a3e47af0eeba3a3e5aa7d311af77001812b8df80fb8beafcd0b40eaf7f1", size = 35816, upload-time = "2026-09-24T21:38:05.798Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f3/181945217690644aa502220a3ec9bf0d2ef9af930bbfffee555fe5236e2f/google_crc32c-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:56610f548f1b35c9568b9d1de30423480f505dae4991556072d5802820ff35c4", size = 35097, upload-time = "2026-09-24T21:39:27.402Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/a2f07f15e624f0de79359b1a6c1deb59ec5061bd3b38744b3b2849400662/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:457d0d9a4718fd52b1494eac5c200ad25beeadbdc91843d550a003910838589f", size = 31958, upload-time = "2026-09-24T21:19:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/923743597b774bbcf12a7c3e00e48d745e15fd616ad7489a40a63fff8f2f/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ccfe40021fd6afe23361175cf7551e3cef5fd34dc1ebe319f14993a83579e0eb", size = 31805, upload-time = "2026-09-24T21:22:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/df/a6/4d0352fe889663e0d81cea7fc664ec9158727384de4a44ab10e9967a7682/google_crc32c-1.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbef61a3794e011c65fb4396a196cf123a7f474fe5a443db8e5dd7d751b9e6d4", size = 38119, upload-time = "2026-09-24T21:38:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e3/26685384e4b66ff0928d9566ef6110a7df76029175a1842329d7e3515f10/google_crc32c-1.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86764b99e7a607830d93cb5b75e0ec3ff6cb06d3c274624418473cee701900d4", size = 36677, upload-time = "2026-09-24T21:38:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/4e90102e84880e97d3cf935f2672ecd29191bdeacf57f01740f92debda00/google_crc32c-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:43a2dc26f9be213fbe0b4fc4a1088c5d45cbfcb3247420ccc820f0fc3edeea86", size = 35096, upload-time = "2026-09-24T21:39:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/0730e1b3a14d054d1466f2fec88dadf978509c749a3d96d8b069cc56d38a/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53fdafef58e230d0c946ab5f8446d123d9f548230a73b29c8b41c9546f268bc1", size = 31961, upload-time = "2026-09-24T21:19:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/d085abaf2fd907121975b92245bb3480fb8be40c37d03f9d6c41857f84c3/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:8b91f41645b15a720357183fa5716682ada441873e3c462c15f9714be36f146b", size = 31804, upload-time = "2026-09-24T21:22:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/94/78/dd1935432337e5da7af391a6fc9f161c1c8e9b9002a402b9190135fe1b59/google_crc32c-1.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16865b477d7941712cb0e0aad8ad4815e984fb5fc16d3fdaef7d986e26e53c95", size = 37775, upload-time = "2026-09-24T21:38:09.249Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/9db03635bb10188d93dcbab9baa2a8670a0da4e868b4370cdbd98d65fed8/google_crc32c-1.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3abb18297d9ef0ab120531838be0e6d68c9fa876570e11c229c48f2edac23ce7", size = 36347, upload-time = "2026-09-24T21:38:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/94dee516c846bd9382c3f566d8f8e5fb9e90599e45afeb697f9fc2533528/google_crc32c-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb63a8d7fa2e95dcff1ca16af2f4d88b526fa5ff72d1696285884ac2d49b6963", size = 35097, upload-time = "2026-09-24T21:39:28.934Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/cb484e8b6174f130f8c6dc79c733a9dd8869b410ad6511fb6104c46b973a/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f1dc17d987ddcc5eba12a7ce48f0eb93141dea236b170c1101151396edf2f0cf", size = 31960, upload-time = "2026-09-24T21:19:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/af/25/3e8e567bd48448e225ea27318ccf2b94e05124e7b8b97b13eaec9e127199/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f894a2877650b56201d26a012a257b76d54a68834dc3913a93830ca8a047b075", size = 31803, upload-time = "2026-09-24T21:22:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/f0/18/bee0dd59ae622482dc6463636c79e4bde7c954d061c859c9256362c9931a/google_crc32c-1.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4488f1553a9ab7e86cdedc833374a7e904031803b995dc0bd0be48c271fa6556", size = 37776, upload-time = "2026-09-24T21:38:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b6/e76e80fed5f2558273c7839e622f98095c9b36c719c7147e38e3c055cb70/google_crc32c-1.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0568b17ed90ac596f29400d99e243fd0cc6276766183def888d1bf8d1dc13827", size = 36350, upload-time = "2026-09-24T21:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/87/34/165542bfa99dfef91a76471cc48cce74b8ff4e295722896087ab2b8e8611/google_crc32c-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:8583ec21d56b565d68ab2963cc7e21b3b271247c29b04286068255ef65f221bd", size = 35090, upload-time = "2026-09-24T21:39:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/43ea41f4061a1cad87b2b6559c98e960e45bf551fe66f83d833b98aaf0c9/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6a3b2c8a343c570ed8100a7627c20badfd92c6caa2067093a86be45af27f5b1b", size = 31963, upload-time = "2026-09-24T21:19:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/a968c0c29ccd2b0c980ff4f9e3f7035cee28c23a1c57541825cc8221858c/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:13179f7e3282617923e957b8e54b8f9c3968030f48640a9f47fd7c5c38c4a215", size = 31805, upload-time = "2026-09-24T21:22:27.917Z" }, + { url = "https://files.pythonhosted.org/packages/03/73/388e493d6c3e252e37165d22efe5a1361f872a24425391b999822861b23a/google_crc32c-1.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:265233aff33d835f5b909584fe36ab29647b598c271b661a300001099109e53e", size = 37925, upload-time = "2026-09-24T21:38:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/36/190d32caa363ef25d685f422ed1bbf93ff1140fb22fd4d90f24cec209977/google_crc32c-1.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dee799544cae42a42b17a88e38b59cf2c271051dc001da2117a8ff240ffa0548", size = 36495, upload-time = "2026-09-24T21:38:14.211Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fd/81cefea6adae7bd92abb23d4567d199f6485a20ec0a305ca5fa04c52b9c5/google_crc32c-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:af73200fa9791ccd380f3598235dba8d82b8af0905df045b3dc60b59836e8ddd", size = 35535, upload-time = "2026-09-24T21:39:30.52Z" }, + { url = "https://files.pythonhosted.org/packages/c5/18/19d4f17f3f33f8fdffcb3e1e69219d6f7ec2c359c160867b04dac1d0a64d/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e6e8be8a94436079cb5340f6d495d9d7ba30124d8b952703994c739c7c06e236", size = 31977, upload-time = "2026-09-24T21:19:03.976Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/8010372c4b46f2ee2352dfdb630c397570cd85522a315df024ad2f9459aa/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:f2b64641bca27497b986b9d87883014035aa904cb4fa333407c6752b3afee9ba", size = 31809, upload-time = "2026-09-24T21:22:29.1Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f8/7e33845d6b90ce1cf37cfabf25cb859277c7d3533ef1b6b1e1ca58581549/google_crc32c-1.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f97c3806dcea41c29c04965347b0e12481561b75e0045dc7a4f69d75dec5d9b1", size = 38359, upload-time = "2026-09-24T21:38:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/556b2423f449a7515af6b8222a4d7833cbe09ff3e8d2f0b80471f5f6d02e/google_crc32c-1.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0abe7e202c25909869c35672ab0f2fe748a7acf276eb78577332a7c38999740f", size = 36858, upload-time = "2026-09-24T21:38:15.799Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/4733f1b7c921d04a2bb9b9916cf66498bf7ad0860a06289413830da83192/google_crc32c-1.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:5695c8b9327e040b2aba12c6659b0acb5995314ef0af0192da66e662e011103b", size = 35534, upload-time = "2026-09-24T21:39:31.337Z" }, ] [[package]] name = "google-resumable-media" -version = "2.10.1" +version = "2.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/f5/f35505e6091614e285056a495488cb0a9c1a9dcc88a4a3c91bbc5fd4835b/google_resumable_media-2.10.1.tar.gz", hash = "sha256:224975032ddb73f7ed9e2f0f4cc08ed1b06874c52d48cc8533e3eb72980b21a0", size = 2164548, upload-time = "2026-08-06T06:24:50.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/4b/44e128cd7b0e72cd26da4a3052cd8a9ef1b5a890be7c611558067ea0b354/google_resumable_media-2.10.2.tar.gz", hash = "sha256:1de441703cd298d75a419bfdc0066e9fc7b0a1de630df96eea8ce8f5c759358c", size = 2164918, upload-time = "2026-08-25T19:19:11.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/ba/77ef49baf338c03a11deadac984e3161b3f2b4fa4bb5aab160e7ca0fd522/google_resumable_media-2.10.1-py3-none-any.whl", hash = "sha256:4e2cbc704207ddc09f23b1f18e8ef4a4ccbfe0f1768b370e5c969704adbd0a1c", size = 81533, upload-time = "2026-08-06T06:23:45.464Z" }, + { url = "https://files.pythonhosted.org/packages/55/30/3e976681b5319715ab022363f58f6f8e10f546a1777756fe3ebd918857e4/google_resumable_media-2.10.2-py3-none-any.whl", hash = "sha256:e3cedc827a4ea41e216582d74346f1fb9fceb625a8c3c53912f2ca1d663334d7", size = 81531, upload-time = "2026-08-25T19:18:07.193Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.75.1" +version = "1.75.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/13/f83676de1dce4f8106bcba91725b3f3f4baf6ca1977685102b008b8e0097/googleapis_common_protos-1.75.4.tar.gz", hash = "sha256:4587babdc82a8d7e5a3d4f5a6697e064bf44a598b4d08341c212b68185eadbcd", size = 154248, upload-time = "2026-09-24T23:20:21.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, + { url = "https://files.pythonhosted.org/packages/11/59/6474c44460037b8474d85c1a82311ec7244f3aabd53197be5b06a4db778e/googleapis_common_protos-1.75.4-py3-none-any.whl", hash = "sha256:e8eb9cffa9a9f3423090cc7a086012004196abab388a0561690e81887fd79cc1", size = 307743, upload-time = "2026-09-24T23:19:57.136Z" }, ] [package.optional-dependencies] @@ -3273,77 +3281,87 @@ wheels = [ [[package]] name = "grpcio" -version = "1.83.0" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24", size = 13428824, upload-time = "2026-07-23T15:20:37.759Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/fd/655c8a773d728bc3c93fb4713ae4bf79ffc75996f86fb78b2974c8e1dfbd/grpcio-1.83.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:fba099b716e73512d61b97f71ea3c31a72abb36904036e316bf4dd148ca8dcc8", size = 6334247, upload-time = "2026-07-23T15:18:53.099Z" }, - { url = "https://files.pythonhosted.org/packages/d4/9a/1ce5760d35a04a992006dd2f79afff2db548f93ee7426fa95c9f1fc90c61/grpcio-1.83.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6755ed67cc3e454d51ae9f6e1915b80d3942fa4de956ef48dacd45ab7f40b727", size = 12168650, upload-time = "2026-07-23T15:18:56.348Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ab/bbcb5be0a1a6cb21f036e2afdd4f7a70147cfb7a7b42648a310d7c43acfc/grpcio-1.83.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5882c1a721b50ce0123ee5e839e1ab059ad72a7ade76cdf2d5bd833b56791acf", size = 6916899, upload-time = "2026-07-23T15:18:58.339Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d2/4c27977ecb3b3f9f363b93f570e001cb24ef264a9a907d7fd0f949ed59f0/grpcio-1.83.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e3eedfc92b6b9f2960115e7e620cf0cbf80bb7849a51ce3820dc54dfd88b6b9", size = 7648761, upload-time = "2026-07-23T15:19:00.071Z" }, - { url = "https://files.pythonhosted.org/packages/23/49/0c823a7627ff2e69a61e4a53c4edf215272892fc2c47c6431f033d46f4cc/grpcio-1.83.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fcaa7c45c45b4a89e2867d1f1785d9481a788399d915e341ed2eb49aeef9dd4", size = 7074920, upload-time = "2026-07-23T15:19:02.293Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ce/963f01ff7c789a76909c9691b704112e02ca1e11c10405cd99c2bd7c40f1/grpcio-1.83.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6b6c666a1d5613ff360c9e90f44665e3a88b25a815209ddbc0917eec281931cb", size = 7598046, upload-time = "2026-07-23T15:19:03.921Z" }, - { url = "https://files.pythonhosted.org/packages/eb/de/1ce6bdefc847a7973040d10cebc8996c653a2a687c0a4da8d05dcab4e397/grpcio-1.83.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6be5c807b717be3dd649446f021301fd7907e376318675d2147823071034112a", size = 8634792, upload-time = "2026-07-23T15:19:05.633Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8b/7fe6a73895e3bdd788101d1276e48e0d262ebb165afacec1ec4efebcd785/grpcio-1.83.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c834e86d8fd2f03d7e4db49a027f7c5b89c5b88eed305543a5295bd6fee61e40", size = 8000286, upload-time = "2026-07-23T15:19:07.739Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b0/9a779de2bcda8722501a056fad1bec3d1117977af0c080ab1fc0655fdf35/grpcio-1.83.0-cp310-cp310-win32.whl", hash = "sha256:35a5b1c192496b6c25956eebfa963468935612206fd2543ac3ce981e6a5e0f03", size = 4404616, upload-time = "2026-07-23T15:19:09.988Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8e/ce9a23590cac33a6c24e6386cc0ffc55821cc13212acc822e98f00a67161/grpcio-1.83.0-cp310-cp310-win_amd64.whl", hash = "sha256:8f6c395e493d20c39b29392ca200e9aaeb78d0bc2f04db0c0a7da7ddc939aa57", size = 5162304, upload-time = "2026-07-23T15:19:11.467Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f6/3b781cd07a715ea5f5125ae264226e7fc4d87603d6d3955022cabfdc5da2/grpcio-1.83.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8ff0b8767ddd62704e0d9571c1890af08d84a3a689ebba1807e62519d0b3277f", size = 6338720, upload-time = "2026-07-23T15:19:13.177Z" }, - { url = "https://files.pythonhosted.org/packages/21/cc/d14833d15d5984e366f1b027fa78bd038c9b028c66880bffb0f5a4d25ee2/grpcio-1.83.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:4772402f43517b4824980be4b3b2274a81eec0004a70009473c31b340d43e223", size = 12178773, upload-time = "2026-07-23T15:19:15.401Z" }, - { url = "https://files.pythonhosted.org/packages/6b/98/8acbb416544e7871132d8e42a07ed70c802d70e6a16c6009e505a34d32a4/grpcio-1.83.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f4cee5fc86e84a0cf7ad1574b454c3320e087c07f55b7df5dc0ac6a873fb90c0", size = 6921203, upload-time = "2026-07-23T15:19:17.824Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/0fdbfaf4fc54e5c88f6bce4008a065092fe7fbc4460eb5617ae8b20fd505/grpcio-1.83.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f5e822a7e7d03282f6ad225e710493c48b9057a353358344a5f7c42b2b37618d", size = 7648508, upload-time = "2026-07-23T15:19:19.685Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ea/107b9dbb2ed3ad14dd774fd3dde7d29ff9938a6c198654becb2c3a0e9a6a/grpcio-1.83.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5f410d7c2903eabb34789dfd6342eef04af1ad459943936b7e09a9f5bd417b9", size = 7079466, upload-time = "2026-07-23T15:19:21.478Z" }, - { url = "https://files.pythonhosted.org/packages/3b/06/9fa9941089e6fae83b060b6ce61c1e81053e52decae43197245f45e07d36/grpcio-1.83.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee94a4016fdf8699fb1fd8a38652475ff677f1c72074cee44deeeb9a7e95e745", size = 7605583, upload-time = "2026-07-23T15:19:23.74Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2f/f10fb56062dc2771c630827a82d9ad0ecd05cad572ea3b08d49f6631680a/grpcio-1.83.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6444666317338e903093c7c756e6cc88eee59f798cb8dd41e87725bf54e1617", size = 8637810, upload-time = "2026-07-23T15:19:25.536Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/f84927258f6a1b6ea6dea661fdc6de859b35e560c96f3012d15ccd39f85e/grpcio-1.83.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa074041231f03959cb097dd5517b0677b8ea49215bae01d5710a7b69dd59969", size = 8008021, upload-time = "2026-07-23T15:19:27.863Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/cdf72161397ccd29d4ca2192f641524536c9cf54ad948c9dd0e0e01138fa/grpcio-1.83.0-cp311-cp311-win32.whl", hash = "sha256:cb056f6e171c42639a50460b2929c82241fda51f71cf3dcdd68090fe45095a45", size = 4404376, upload-time = "2026-07-23T15:19:30.137Z" }, - { url = "https://files.pythonhosted.org/packages/df/ed/e0ffeb4c848699c194dc9fb6a29ab29bcb2b6aac8c416bf18c51bfe8242c/grpcio-1.83.0-cp311-cp311-win_amd64.whl", hash = "sha256:7416952ca770477990257206276999056f8316d79196f2f25942393e58a20b49", size = 5164469, upload-time = "2026-07-23T15:19:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/15/2b/51e32514a4e9b715375c99721aadff0f24164cc2049b8269eda4de82a814/grpcio-1.83.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:28f6c35ac8fcf10e4594f138e468f194360089dde40d126a7033e863fc479930", size = 6303167, upload-time = "2026-07-23T15:19:33.78Z" }, - { url = "https://files.pythonhosted.org/packages/39/33/b5b50fc2c6fbe350e04814047bb2d409feec7b36ef8b170254c050e06bc0/grpcio-1.83.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:33898e6a28e4ae598f1577cb1c4fec2a15c033d0ec52b9b45a09610dd045b9da", size = 12160538, upload-time = "2026-07-23T15:19:35.958Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5f/734e72e7b9f79bcf0b2c270b8d3bca0e4ebb97a27a50d06240b145f6d41e/grpcio-1.83.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fb8a1dd0c6f0f931e69e9d0dc6d1c406ed2a44fa963414eafba07b7fb685d16", size = 6869310, upload-time = "2026-07-23T15:19:38.607Z" }, - { url = "https://files.pythonhosted.org/packages/a4/17/a1735f215b2a5cd43c38b79eac072ad197e61be9829905b6b29550abd0db/grpcio-1.83.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2b5e75c34842cd9c1b95285ca395c6a569664b81e3ffa6b714125922942abaaf", size = 7613472, upload-time = "2026-07-23T15:19:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/b2/78/c9e81f806ac704b6b145cb01628db398985b1f8dfdc10e23b55fb0902b3d/grpcio-1.83.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeb339838db07600481ef869507279b75326c75eac6d10f7afa62a0da1d2bcdd", size = 7040616, upload-time = "2026-07-23T15:19:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ba/94cd5af859876049d340480acbb61a959096c84b567f215534faa78d0424/grpcio-1.83.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f47d62808b4c0a97b78bff88a6d4ca283a2a492b9a04a87d814af95ca3b9c19c", size = 7570491, upload-time = "2026-07-23T15:19:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/3e/15/108d30d5a5c964312ae8b9cb0e8cc5b3c1cc68d8f757cca52b3565534d26/grpcio-1.83.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62003babc444a606dcd1f009cd16391ce23669ae4ad6ec267a873da7937a69f5", size = 8605036, upload-time = "2026-07-23T15:19:46.454Z" }, - { url = "https://files.pythonhosted.org/packages/ea/23/3828ae13c3db8233d123ad612747665817b952d8a954f32390230b582336/grpcio-1.83.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1aa567f8c3f19850ffd5d2858c9a8ea7c80f0db6c01186b71eb31e923ec984f5", size = 7981587, upload-time = "2026-07-23T15:19:48.913Z" }, - { url = "https://files.pythonhosted.org/packages/17/5b/77af31228f55f55a2a5112bb0077ad0a1c4d23dbb0c2853a62475bbdcc14/grpcio-1.83.0-cp312-cp312-win32.whl", hash = "sha256:cb2906c61db4f9c64cc360054b5df70eeb81846228e9e56a4944bd415a63dadc", size = 4394004, upload-time = "2026-07-23T15:19:50.618Z" }, - { url = "https://files.pythonhosted.org/packages/c0/da/f706e39550e7a3732ce2b9c5926107a93d74a802775b19b642a6df27dc96/grpcio-1.83.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c699bbb20f143c8f2bff219de578aa2dc1f919399d67dc702b038b986ee62df", size = 5158525, upload-time = "2026-07-23T15:19:52.246Z" }, - { url = "https://files.pythonhosted.org/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0", size = 6304480, upload-time = "2026-07-23T15:19:53.962Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1", size = 12154419, upload-time = "2026-07-23T15:19:56.3Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867", size = 6873200, upload-time = "2026-07-23T15:19:58.701Z" }, - { url = "https://files.pythonhosted.org/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881", size = 7618811, upload-time = "2026-07-23T15:20:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f", size = 7042310, upload-time = "2026-07-23T15:20:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61", size = 7575412, upload-time = "2026-07-23T15:20:04.712Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45", size = 8604248, upload-time = "2026-07-23T15:20:06.77Z" }, - { url = "https://files.pythonhosted.org/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c", size = 7977458, upload-time = "2026-07-23T15:20:09.109Z" }, - { url = "https://files.pythonhosted.org/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf", size = 4393993, upload-time = "2026-07-23T15:20:11.017Z" }, - { url = "https://files.pythonhosted.org/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735", size = 5159650, upload-time = "2026-07-23T15:20:12.979Z" }, - { url = "https://files.pythonhosted.org/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa", size = 6305607, upload-time = "2026-07-23T15:20:15.353Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c", size = 12166617, upload-time = "2026-07-23T15:20:17.435Z" }, - { url = "https://files.pythonhosted.org/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b", size = 6880213, upload-time = "2026-07-23T15:20:19.98Z" }, - { url = "https://files.pythonhosted.org/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c", size = 7618335, upload-time = "2026-07-23T15:20:22.161Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df", size = 7043416, upload-time = "2026-07-23T15:20:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b", size = 7583253, upload-time = "2026-07-23T15:20:26.313Z" }, - { url = "https://files.pythonhosted.org/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404", size = 8605102, upload-time = "2026-07-23T15:20:28.437Z" }, - { url = "https://files.pythonhosted.org/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af", size = 7979826, upload-time = "2026-07-23T15:20:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33", size = 4471765, upload-time = "2026-07-23T15:20:33.195Z" }, - { url = "https://files.pythonhosted.org/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9", size = 5298932, upload-time = "2026-07-23T15:20:35.48Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4b/a0dc421d049b743093eae90caeb5dd92ced7226cd4919dc4de34c81455b6/grpcio-1.84.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:71fd60e6e426d293d0a2f685115ad0a0845117602cf13605a4be7524fb5f7bba", size = 6450049, upload-time = "2026-09-14T06:56:48.72Z" }, + { url = "https://files.pythonhosted.org/packages/f7/41/90292bf55af7aa09de0e3ec928d1b8c56d477f85244f7928d2231630b781/grpcio-1.84.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8e1a45d174b6b8589f51dce1cea804aa6c1f72c9c80cba91ae2caabeb6d90540", size = 12344932, upload-time = "2026-09-14T06:56:51.685Z" }, + { url = "https://files.pythonhosted.org/packages/e9/68/b6c0248266a378b1bde08e4de7d69f3cc08ee6f5937a5dce7d3c3ba0fe1d/grpcio-1.84.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efb29f8633bf6630dc89de4fe0353ac3d7e4b70ef7b6e29fb40f00e68c127fa5", size = 7030162, upload-time = "2026-09-14T06:56:54.412Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/0a2a2cbcf48847f83eb51fb982116d0965f2fe068e73f28b2d17facf30a4/grpcio-1.84.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d0fdd25faece8a1f95e8a3a8006e29701b5cf8dadb4a8132e68f3134637004a5", size = 7781546, upload-time = "2026-09-14T06:56:56.571Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7c/da97476f3c2e90e9f00bfb19def7cbb5f841b7661e3cd09c6a89beaa5b98/grpcio-1.84.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:393d8a78bff6731ecc5ad2151a821f8fbc1709b137ebb9c25a4ef399fbdcc914", size = 7186279, upload-time = "2026-09-14T06:56:58.817Z" }, + { url = "https://files.pythonhosted.org/packages/14/16/27fa3aed1ee6fdcbb978a1bd4bce255dc0122b90179535177a96543d14fc/grpcio-1.84.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fc66cb50c93554b86db0b6625ab5c6e9051dbf8847c08d93c84918e02e413fb7", size = 7731191, upload-time = "2026-09-14T06:57:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/4c/78/75644af37af85afb381376aef99cad92da8bc2d56ba3e5ae070a7cb59682/grpcio-1.84.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:455ed6083353b8e938f1d58c765eab2fbb165731e5b507be30fee344915a2a11", size = 8790443, upload-time = "2026-09-14T06:57:04.623Z" }, + { url = "https://files.pythonhosted.org/packages/95/4d/ce57fa986e93c06ef867f64e1ebe419e924fdc2395115607f0725f4855e4/grpcio-1.84.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d6a82c4fc6c85f2fb7572c86bdb86f84c97b6580e5f6599f711800bac48a5d8", size = 8138068, upload-time = "2026-09-14T06:57:07.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a6/22a73111c4f75da9450bf0481fac805396ec9bf6f949a90bb2969de07cf4/grpcio-1.84.0-cp310-cp310-win32.whl", hash = "sha256:8e3f508d0e9e6236ba2f08d56e33355e434e785e813149a1b8477d3edf69779d", size = 4496545, upload-time = "2026-09-14T06:57:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/ff/dc048bc3d8ebd8d4b7f6f6803c76142a9a5ca1e1e9fa34e79597f0f9ed77/grpcio-1.84.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed2c1493c44d0932f1e55fdb5d1ead658c68288ec5d51b8c4928422d98633ef9", size = 5258144, upload-time = "2026-09-14T06:57:11.403Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] [[package]] name = "grpcio-status" -version = "1.83.0" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/fd/848dd7e009de85f8ca59999d1cc618ff8ebf7ea5636d083a47455d212d24/grpcio_status-1.83.0.tar.gz", hash = "sha256:837219c6de9afdccb6f6f72b34bc71e151a2011ef04040e3faaca746a57e54ae", size = 13965, upload-time = "2026-07-23T15:24:26.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/00/73204406228cf989bea6b0fd9fe4702fab49a8a152a0c6f90856dadb6ac7/grpcio_status-1.83.0-py3-none-any.whl", hash = "sha256:f6a838a7c5fb84ae98833ec0ef81ed438c26e11e54b2ddb8e92ad328c861de69", size = 14636, upload-time = "2026-07-23T15:23:49.044Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -3372,20 +3390,20 @@ wheels = [ [[package]] name = "idna" -version = "3.19" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] name = "imagesize" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] [[package]] @@ -3583,12 +3601,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "iso8601", marker = "python_full_version < '3.11'" }, - { name = "os-service-types", version = "1.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pbr", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "stevedore", version = "5.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "iso8601" }, + { name = "os-service-types", version = "1.8.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pbr" }, + { name = "requests" }, + { name = "stevedore", version = "5.8.0", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/d9/a01a3898657626cbcca9fa0e8fa58facb4632e172bdac622d47950f7e12a/keystoneauth1-5.14.0.tar.gz", hash = "sha256:7b942084d3db27dd285c13253cdfee10b05be0436db1c01e15dc744902197a7f", size = 288739, upload-time = "2026-05-13T09:09:26.125Z" } wheels = [ @@ -3597,7 +3615,7 @@ wheels = [ [[package]] name = "keystoneauth1" -version = "5.15.0" +version = "5.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -3606,15 +3624,15 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "iso8601", marker = "python_full_version >= '3.11'" }, - { name = "os-service-types", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "stevedore", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "iso8601" }, + { name = "os-service-types", version = "1.9.0", source = { registry = "https://pypi.org/simple" } }, + { name = "requests" }, + { name = "stevedore", version = "5.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/627b01cde69d0ece2fd552b8c7c34af06acf13a0a77d1829ff0b46a3b45f/keystoneauth1-5.15.0.tar.gz", hash = "sha256:ce2cacdfd028e65bd23ff403d6572ebfab3b006d6d2dde3aa85c263675a9fbb5", size = 288544, upload-time = "2026-07-03T16:21:31.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/84/a76c0819add727693d89b152c11452650036fcd481274756a33172e386e7/keystoneauth1-5.17.0.tar.gz", hash = "sha256:82359acc20c754fcb22818e090e2fea647e4c5c1137a6addb4984e9fba708ab3", size = 316782, upload-time = "2026-08-28T16:16:24.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f6/6552fed59449604d982ffe9fe202b82f0724d333319812d1f544a95d3a09/keystoneauth1-5.15.0-py3-none-any.whl", hash = "sha256:9e2a4cf45cc2e2164093157091bc073eda1f334f755e8fab9fd25420c6f6acef", size = 342364, upload-time = "2026-07-03T16:21:29.866Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6b/e6eda650bb709c23688e250a7e66ad3ecbd470b70597d35d051f11ae66fa/keystoneauth1-5.17.0-py3-none-any.whl", hash = "sha256:28201a5299d4fabc478dda28e523a0d12777cda47e53b234772d15d3e0771736", size = 366027, upload-time = "2026-08-28T16:16:23.54Z" }, ] [[package]] @@ -3708,11 +3726,29 @@ wheels = [ name = "markdown" version = "3.10.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, ] +[[package]] +name = "markdown" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/4f/700155c8c20d9e655dd0732b5fc3c7614f291b9148da271d7388e50bf774/markdown-3.11.tar.gz", hash = "sha256:180224db6aed87ba9ce1f2781ebcd5826253de8ff637112090e24b84502bbf9f", size = 485623, upload-time = "2026-09-25T13:46:23.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/1e/32971905a7ab47f8b66866ed949fa48b104ba1c4a6fa57794c4f2c4b2cb8/markdown-3.11-py3-none-any.whl", hash = "sha256:cd6c89e7eb308c8b332ed673215a52d208a43f8bacc030b1419376129408719e", size = 111296, upload-time = "2026-09-25T13:46:22.163Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -3869,16 +3905,16 @@ wheels = [ [[package]] name = "msal" -version = "1.37.0" +version = "1.39.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/99/d840198ecf6e8057bbc937f129ae940404485d736cda73253bbff9537f01/msal-1.37.0.tar.gz", hash = "sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec", size = 182444, upload-time = "2026-05-29T19:49:05.561Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/85/747d28986a44b715cc51cf57aa021c8fd94e9614064261ceec77320e8b0b/msal-1.39.0.tar.gz", hash = "sha256:6ab7de335e6d7f5717e2c7e1dbf86e4dda2f6acf3c56773b78dc53ebc6395b5f", size = 205360, upload-time = "2026-09-17T16:07:45.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl", hash = "sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d", size = 123725, upload-time = "2026-05-29T19:49:04.335Z" }, + { url = "https://files.pythonhosted.org/packages/a4/32/178457385925ce301c7b3232bd85665d4df57da39568b62dcfc74031873f/msal-1.39.0-py3-none-any.whl", hash = "sha256:2d2577886906cd7293850dffa2da29119966c213bfc6ec0cecf8bf7621e1ca77", size = 131629, upload-time = "2026-09-17T16:07:46.722Z" }, ] [[package]] @@ -3925,140 +3961,183 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.1" +version = "6.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, - { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d6/99/1d4d69c3512d0ddbfa3a1b69cfd9a151012ab2eb4eabbb096201b1f0b7d8/multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2", size = 182404, upload-time = "2026-09-21T17:59:05.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/1c/fb2114207afc7f00ab9897ae5beeee27de5ec58ea8b9623e879f2c56f003/multidict-6.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8", size = 97307, upload-time = "2026-09-21T17:54:14.957Z" }, + { url = "https://files.pythonhosted.org/packages/b7/23/1ab8c7349adbb30c8cf41b88b0794740df8ea9f7de2e382da040a2f5bd8e/multidict-6.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19", size = 58830, upload-time = "2026-09-21T17:54:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/e0/89/b845535014a9ad50537ca50204aaf6351c5368822a3acd9da854fb2a5a34/multidict-6.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa", size = 57645, upload-time = "2026-09-21T17:54:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4f/28ebb40a6f8e8aa7be78bcbe4bf9a1ac9518c40ef88accf2d4cd6ed7a7e0/multidict-6.9.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299", size = 301535, upload-time = "2026-09-21T17:54:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/33/51/0de5fc980df34d1ffed9ef68f60ce86c2d4baa3d422598a53f8730c56757/multidict-6.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96", size = 298796, upload-time = "2026-09-21T17:54:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/dd86067bdd2e961fdee8ec659fabc50f4e76403616b508714ec2686b0a49/multidict-6.9.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188", size = 282392, upload-time = "2026-09-21T17:54:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/2f/52/5464a2675579e3adb48b7cd4707a59da499660f0e22c024bc7281d2d9bdf/multidict-6.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a", size = 314538, upload-time = "2026-09-21T17:54:23.366Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/7f2b3fafe064181669157ce872a097b08a0546fb6fd2481e8c5604b47ec3/multidict-6.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e", size = 315675, upload-time = "2026-09-21T17:54:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/bd/53/f65ed68b96eea2b4f6fa4f9031bdfccd8f3097c429e3d371c545d1c40ca4/multidict-6.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab", size = 303685, upload-time = "2026-09-21T17:54:26.236Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5f/ade05795c6fb9adfccd61695a0f0509bc793583f589c4b1b87f27215a139/multidict-6.9.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88", size = 280306, upload-time = "2026-09-21T17:54:27.685Z" }, + { url = "https://files.pythonhosted.org/packages/60/b2/c996349a61e0f43c7c95877943a67476261bf906ff993670afbd33ff5bf4/multidict-6.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1", size = 291691, upload-time = "2026-09-21T17:54:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/8b7016d1084854729c80acfae71c5a51ea7e7c9411d66c3e2729772f5723/multidict-6.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c", size = 290357, upload-time = "2026-09-21T17:54:30.491Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/801653b6aa9343a7430cecdefa1bf71ff24432224c02f15cc15e47837652/multidict-6.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126", size = 305831, upload-time = "2026-09-21T17:54:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f3/9fc997861920bdbf70db82bdce5c802ec6477754c4bd78cff2898e3c63f9/multidict-6.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410", size = 308995, upload-time = "2026-09-21T17:54:33.571Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5f/278b5a2c99d6de27914b49158dd6f2a6481e53359142cc366dff04821a2f/multidict-6.9.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5", size = 279240, upload-time = "2026-09-21T17:54:35.025Z" }, + { url = "https://files.pythonhosted.org/packages/3d/db/0922594c601a691eb4365a83f4f92104ef87a9728d8066fa25c99349e754/multidict-6.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4", size = 305487, upload-time = "2026-09-21T17:54:36.555Z" }, + { url = "https://files.pythonhosted.org/packages/64/cb/07bca86d4a361a87128f3019540d7689800fdcad44e3b3ad08f1f035ed74/multidict-6.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c", size = 301997, upload-time = "2026-09-21T17:54:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/99591bf626613ea35779d2460949b45e59e589dc9058d6693c923eccd4e5/multidict-6.9.1-cp310-cp310-win32.whl", hash = "sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9", size = 51205, upload-time = "2026-09-21T17:54:39.152Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/10c008aac77297aa618631e8726ece31668f9dc9d9c848e4b5d2e68eb3d3/multidict-6.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0", size = 59049, upload-time = "2026-09-21T17:54:40.465Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/da73a58bef2613571902a824f1e11185207ba756215c99038f6d220bc167/multidict-6.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2", size = 54610, upload-time = "2026-09-21T17:54:41.605Z" }, + { url = "https://files.pythonhosted.org/packages/e3/24/3823efc330630a1f132bcd0a9b182ddec3c53f452f551c8e8d3aaf5a2d20/multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4", size = 96739, upload-time = "2026-09-21T17:54:42.815Z" }, + { url = "https://files.pythonhosted.org/packages/6a/69/36331fe1d3aeb0c525a9972d6cf82791075a395aa96f026f060009d691ae/multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987", size = 58514, upload-time = "2026-09-21T17:54:44.064Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ca/5860651f782078ef13f2761c2593f4ba066d76f626b2f30f23a7f838b775/multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c", size = 57303, upload-time = "2026-09-21T17:54:45.304Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9f/c56e2fa223cb5d182660e1a09eec38880f96b7cd22769582f74447c60418/multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1", size = 320076, upload-time = "2026-09-21T17:54:46.826Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/eda4fe0cad51f96096363ed32d3e0f8df28dab00beff4d92adc9db517726/multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07", size = 315489, upload-time = "2026-09-21T17:54:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/14/10b1ded0a4085a51c3054125f854da4f47ce4fec724a2d2503231b83f3db/multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1", size = 296716, upload-time = "2026-09-21T17:54:49.749Z" }, + { url = "https://files.pythonhosted.org/packages/dc/14/04d155dd18528443cb9009f3a9f35f7417773797d072c95104ff17b8b9b7/multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3", size = 330244, upload-time = "2026-09-21T17:54:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/8b/90/284d10b3a9e5b312a8ec5b7a175fc7d560eefdc99886320449891a197992/multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80", size = 331288, upload-time = "2026-09-21T17:54:52.595Z" }, + { url = "https://files.pythonhosted.org/packages/27/50/f420de3683f9b047fc5588064d84043ef57a7152ef451b8373dfc7068d22/multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387", size = 320663, upload-time = "2026-09-21T17:54:53.91Z" }, + { url = "https://files.pythonhosted.org/packages/01/ab/0120a650d7ce4fe167299c13a13cc4ee2dc72458a9c0e9e9330b824b73c9/multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345", size = 291139, upload-time = "2026-09-21T17:54:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/c66342f73ee5cd2c9b1dda6cfce89b5f348c48921b8c8b6e59aafbbfe31a/multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd", size = 310044, upload-time = "2026-09-21T17:54:56.745Z" }, + { url = "https://files.pythonhosted.org/packages/05/0d/e44b90d9e77e44ec3473c4c7ac7ea95764d7e8fbf6d5a085ef9bff7ce0c2/multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f", size = 303279, upload-time = "2026-09-21T17:54:58.178Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/7741f7c23c211fae31ab546f0ba41569dae7671cf7ca30d5afb59e9d92aa/multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b", size = 326177, upload-time = "2026-09-21T17:54:59.825Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/0cdbfbbe36316b2ef4b8e011afaca2cf291a73fe4fabf34dd0b22e3ee48a/multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82", size = 323526, upload-time = "2026-09-21T17:55:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/0c86e9678e78f4596df0ed7f59d2aa755107da3f2a92f1b7fa150e94cbb8/multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953", size = 290278, upload-time = "2026-09-21T17:55:02.993Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/71bd7f43c4883cba6c5f96db88bd2311d52e74ad7680be5915938490d56f/multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a", size = 321142, upload-time = "2026-09-21T17:55:04.48Z" }, + { url = "https://files.pythonhosted.org/packages/2d/00/bf59d6bb22a4c152e4039574c29490caf3c65107715121807b4bd7cde244/multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13", size = 319045, upload-time = "2026-09-21T17:55:06.119Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3c/f27f4f045198bf1c4bcd6eb3b6cf06be2dceaceb6e67222b903fce7a6749/multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f", size = 51309, upload-time = "2026-09-21T17:55:07.531Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/c2064aef64efc007bc89a571f843e5772f27946eb9bc018731bf8016e319/multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e", size = 59148, upload-time = "2026-09-21T17:55:08.936Z" }, + { url = "https://files.pythonhosted.org/packages/45/a1/3bab1827edd813dfb90712af5cbb7fc1473ed4d9c871d103df4f4bb950c2/multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d", size = 54258, upload-time = "2026-09-21T17:55:10.191Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0d/4b5afb6d3e545c9af0cdfe2db8f6f4c6664568c23d863d888674e447e6a4/multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe", size = 98360, upload-time = "2026-09-21T17:55:11.508Z" }, + { url = "https://files.pythonhosted.org/packages/89/ab/1b9ca66251899981b21138b87da9d5a9c2c81af12b1ea7d19466972f7fe2/multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006", size = 59979, upload-time = "2026-09-21T17:55:12.866Z" }, + { url = "https://files.pythonhosted.org/packages/36/eb/6ae44062466c26c8469ef43f2481a6a48d8cea0587b2d54514ec92e2adfd/multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42", size = 57736, upload-time = "2026-09-21T17:55:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5c/a67817593019257a4ac8b0d1b4c426030e637c047b0692ef405439ecea7a/multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5", size = 333097, upload-time = "2026-09-21T17:55:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/599ae2e6222822d88a247a8a7ae82fe6fd25d5700757b79603d5edafe6a0/multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea", size = 334196, upload-time = "2026-09-21T17:55:17.015Z" }, + { url = "https://files.pythonhosted.org/packages/15/10/d8aac5acacbe7f5c117866c776ec26d5f15a868759b6f37ad8e7ed3b5b02/multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24", size = 317465, upload-time = "2026-09-21T17:55:18.412Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/714f796f7293a8b1c5c3f465a26996231d5450c54e85d64ef1258c091134/multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24", size = 345914, upload-time = "2026-09-21T17:55:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b1/e37fbf769c567be277bcf32df6234035a4384677fcc1bd852752be3d6b93/multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a", size = 351163, upload-time = "2026-09-21T17:55:22.21Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5b/db08419c1e1f7c9d60cfd2787b2b517d7ae4ebbda8281b48a33eb5141467/multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d", size = 336881, upload-time = "2026-09-21T17:55:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/cc9bc8a62651d2d53ab93ee4993b3a71b7cb78eb8ebfc9c757a5b6698617/multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805", size = 303405, upload-time = "2026-09-21T17:55:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0c/8e912afafa70e944dbb8bec4b66ca6e008511395278c0d3dd0e89567536a/multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02", size = 324265, upload-time = "2026-09-21T17:55:26.989Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/62c2af80fb085e6234805017af857b8e913dfac7a55e9c1349c27768c58a/multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8", size = 322085, upload-time = "2026-09-21T17:55:28.732Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6b/35bf801b336fd960811207203ffdcdc24acc3558b3e7ff2e1c914b141e70/multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580", size = 338107, upload-time = "2026-09-21T17:55:30.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/495ef65bf5bba29d142d81b3fe1b8154b919bed70e96491b1e93c3c26f0f/multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004", size = 339631, upload-time = "2026-09-21T17:55:32.012Z" }, + { url = "https://files.pythonhosted.org/packages/19/bd/057fdff5f4e04dcd40a960e38f77d19d3c4b67dd243ffa5f43718edb29fc/multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60", size = 302080, upload-time = "2026-09-21T17:55:33.817Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/9ace933ee8dad808632523726f42255b09600087219e3d4ead7369820910/multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a", size = 341107, upload-time = "2026-09-21T17:55:35.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ac/7c1204406097bfc5c283d4a3287d61166807d189917567df4c1318484fc4/multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6", size = 333482, upload-time = "2026-09-21T17:55:37.002Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/a70c32ea3299ebe00f44533740cb46905717c51faf29bbd3ce8bb5886d9d/multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb", size = 52548, upload-time = "2026-09-21T17:55:38.529Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2e/8c9c2591df01e5692ad1bf92febb082a17b1c6c3a19aa8b7ff49988fd989/multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203", size = 59622, upload-time = "2026-09-21T17:55:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/d0/95/59f4472ec512bc180fd207899594c7803ece96262ff9aaa3a4b6d7da6940/multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc", size = 54930, upload-time = "2026-09-21T17:55:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/ddf7c76860f5f553a23ad3ca38463ccf5247081618977742c8dc4625355e/multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3", size = 63266, upload-time = "2026-09-21T17:55:42.676Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d3/f4ae5945ea2de597eaeb4eca3a74c59783ace54482c9b49435442b63efa8/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382", size = 55771, upload-time = "2026-09-21T17:55:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/93/7d/15468239920040d01c686e5ae669e6382f7bf31fc3e5e0a6b8f1c3b32d3b/multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a", size = 57116, upload-time = "2026-09-21T17:55:45.315Z" }, + { url = "https://files.pythonhosted.org/packages/17/1b/b958f06aac2d8b1e485eb1c105b1b159cbf3868249e5142142451577dad2/multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40", size = 97692, upload-time = "2026-09-21T17:55:46.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/6c/d6cfe18e61010166d7237d7527c775eb9843e7078feadea45b7628751b60/multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8", size = 59556, upload-time = "2026-09-21T17:55:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/46/46/9b4c1127cece0289fb207d04aae5116382ddfcb2a4dc8e0cb49a33f3c7b3/multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c", size = 57357, upload-time = "2026-09-21T17:55:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fc/c18b07100a6064573e49e538d13d47eb2b5221f48080512df78aef54ab04/multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a", size = 330433, upload-time = "2026-09-21T17:55:51.435Z" }, + { url = "https://files.pythonhosted.org/packages/62/ff/52a0082adeb69656b634609d4fb0ae65456deaae4cca3d5a12656626dc50/multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8", size = 332487, upload-time = "2026-09-21T17:55:52.937Z" }, + { url = "https://files.pythonhosted.org/packages/39/3e/80bd4729635a7af371ff4dcc312594cf96e73f98854491be8740792de430/multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94", size = 316911, upload-time = "2026-09-21T17:55:54.751Z" }, + { url = "https://files.pythonhosted.org/packages/15/f4/7ea4a907e053e924e2e1694d90560f79f72056504bfd5e8483695f9527d2/multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa", size = 344950, upload-time = "2026-09-21T17:55:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/0d/16/9642ae41fbfbdc546aa481c2b07ae1bc087a94ab5f5020b4d9ab77da401f/multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5", size = 350264, upload-time = "2026-09-21T17:55:58.073Z" }, + { url = "https://files.pythonhosted.org/packages/93/f2/e06c8e42074d0a4b8419bfe92afbd1d262190b69549ecbcd2dffa4f103c2/multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c", size = 336213, upload-time = "2026-09-21T17:55:59.723Z" }, + { url = "https://files.pythonhosted.org/packages/98/43/d7cf9ef4700e7c25244590d1b4f20cf12cbed86beba6c17be4d9e48c1099/multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b", size = 302555, upload-time = "2026-09-21T17:56:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3a/54209920324bc3928f5f2ba28b01a6dd957a9d48c3aaff77e38faaf5668d/multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05", size = 322247, upload-time = "2026-09-21T17:56:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/33/50/df96f961b178b621ab0adbf9221d4fd21534e1064fd9f5e85c1fa27fea55/multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a", size = 321787, upload-time = "2026-09-21T17:56:04.894Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/37f354562a8f82f9c1f63d3a94300fc82126d50320175f0702bbd544f87c/multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418", size = 335498, upload-time = "2026-09-21T17:56:06.783Z" }, + { url = "https://files.pythonhosted.org/packages/1e/44/78e366efc6c004185295cc9eb9711c10f13eadf3b547603b9c5526329976/multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1", size = 338244, upload-time = "2026-09-21T17:56:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2b/ab5bd3964691abe4d14b43bdbdb8a621b77cea2ca352dc93159ec0a4575b/multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721", size = 301641, upload-time = "2026-09-21T17:56:11.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/f6bc899f5aafaba755edc8e4bb934bc00b1e405f8d3fbc1dd10283738074/multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771", size = 340162, upload-time = "2026-09-21T17:56:13.076Z" }, + { url = "https://files.pythonhosted.org/packages/eb/03/a8fc809ef364b8c231c065ea2ea2f97ae86d4f9a0e36b5721759fd606778/multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14", size = 332826, upload-time = "2026-09-21T17:56:15.175Z" }, + { url = "https://files.pythonhosted.org/packages/f2/27/32dde80245e2024e9bb7a6ca3c2fb7c695dc74014cba557cc35be6f91e24/multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40", size = 52378, upload-time = "2026-09-21T17:56:16.959Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/1bac2987edac273a6b27fa50e9204bc3466c94a7b4f6a44828d730f8810f/multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d", size = 59449, upload-time = "2026-09-21T17:56:18.336Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/2a77ce19eea48cb9b3521a202b513ba3349aafa702c8317be0b645ffb74b/multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4", size = 54713, upload-time = "2026-09-21T17:56:19.774Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/c6041015aa2cdc11e1dff0cae58898ae5525d19e35efd65a4ecf3c6ea235/multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5", size = 62336, upload-time = "2026-09-21T17:56:21.181Z" }, + { url = "https://files.pythonhosted.org/packages/23/db/95bd0afb130a0149c955f1f066143528d60864e64f41ff5a1e1313609361/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9", size = 54892, upload-time = "2026-09-21T17:56:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/94/cc/8aa34ae8d09498e8da003f485c03371b55cf1bc6d56cff861b3a7de61251/multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca", size = 56251, upload-time = "2026-09-21T17:56:24.36Z" }, + { url = "https://files.pythonhosted.org/packages/52/48/22baa3b95375096369b04c4348af14139570f302f951773d2c68bf0ccf2e/multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840", size = 96345, upload-time = "2026-09-21T17:56:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3e/b96779dcac28ec6d491fd821112a0156b519b6701bba186cdf6f6df737f4/multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800", size = 58901, upload-time = "2026-09-21T17:56:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/8e6d950f02ca6ecc00ecf671f2ddb5dab7017671a8d197326f75621f5786/multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d", size = 56491, upload-time = "2026-09-21T17:56:28.901Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8f/212ed7e03282c7bc271e3796bf985b53d5727c2827361e3dd9c5ee6b093f/multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201", size = 332297, upload-time = "2026-09-21T17:56:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/a0/97/555aab000e03ecba85c0a1837fd5674d8bf10949490df7800da3a40660ce/multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701", size = 329197, upload-time = "2026-09-21T17:56:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/3d/25/f3539b5bdc147beba66015795dfe589eedcc580348fb1a8c69b1df4196b5/multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610", size = 308456, upload-time = "2026-09-21T17:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7d/4665cad8fc326787d218879be879d86dfe32f006c1f88ae47b04d3b52fd5/multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694", size = 343382, upload-time = "2026-09-21T17:56:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/41/01/e1e9abe27f492b22dede492bc423015597412b14ce074f7475f4ecd80185/multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a", size = 346848, upload-time = "2026-09-21T17:56:38.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/8d54118bc228e64e1087f1729647b93bbea225eda4a3f914663a6f410bca/multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a", size = 333612, upload-time = "2026-09-21T17:56:40.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/9509dafc1fc68948e75842a74533ddfa64af57f2b17810e9cacfa68a829b/multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba", size = 304246, upload-time = "2026-09-21T17:56:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/96b93808e1bec0b51d49d89859e49b82a94f7b2e58eca0fc44ffcafd3ae5/multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290", size = 321983, upload-time = "2026-09-21T17:56:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1c/08e5184df026b9d8e05a6dce5da22628e46bc4a7c7f03b8bf3c6939a8c46/multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d", size = 315927, upload-time = "2026-09-21T17:56:46.208Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5d/9ff39e7387f6fcc9a81af5dbf055fde08149849a04f8df10ca5b086b0d39/multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828", size = 336827, upload-time = "2026-09-21T17:56:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/30fbb2cab22203128928066de94be247dc9a3230bf2bfc54437ac3b5f9f7/multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132", size = 336317, upload-time = "2026-09-21T17:56:49.778Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/2480aef6b7d8e7ab89b064ae14f12ec069570dab3a5c13e0155912293247/multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b", size = 303258, upload-time = "2026-09-21T17:56:51.829Z" }, + { url = "https://files.pythonhosted.org/packages/4c/04/dbe59cf4ea3778600e8e70e30b31aa9118ad6da2844bc9f717804cbaa000/multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d", size = 337232, upload-time = "2026-09-21T17:56:53.537Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8c/abe55548f06f12878588f08eb803c5a391283f01243f784cb00adac79231/multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194", size = 330261, upload-time = "2026-09-21T17:56:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/ba67f668478f152bdfa18abe72e7588bd078bf8b42c172fcf542fcf45826/multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841", size = 51715, upload-time = "2026-09-21T17:56:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/014bb2128aedc8157d2848f0ec8a0209c6b822fb88deea012834e1c4a4e8/multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3", size = 58621, upload-time = "2026-09-21T17:56:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/8f43c03c2d8821a2320b8c560d8a49d0be2920627edfac3f546922f91969/multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9", size = 53959, upload-time = "2026-09-21T17:57:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/4f2a5eeea10b6d42634ba168ec6efc732ef218772e57b8239cd485c202f6/multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76", size = 107694, upload-time = "2026-09-21T17:57:01.785Z" }, + { url = "https://files.pythonhosted.org/packages/32/ce/c68d08ac2096c1528ff20d77fd10f7e401db3e534ac5a1c7726e36a77c2f/multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b", size = 64766, upload-time = "2026-09-21T17:57:03.52Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9a/9f5c270c88fe289b4876fe28f9932a70e015a7916aff1e5b0325f7597b11/multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485", size = 62723, upload-time = "2026-09-21T17:57:05.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/63/c9a0b131354dd949426166cef712d708f436a8cf261114f8d5d2aec440ca/multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455", size = 290445, upload-time = "2026-09-21T17:57:06.687Z" }, + { url = "https://files.pythonhosted.org/packages/9d/41/ed27d1d20ba91f4e13e8f7dcfaa592614ae0e1d5f45e03171d23c3b098ac/multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125", size = 304375, upload-time = "2026-09-21T17:57:08.717Z" }, + { url = "https://files.pythonhosted.org/packages/66/6f/a7b26167173c3b73d6e5ebb0856c5154ad005156c3a2fa4062329921819c/multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390", size = 281746, upload-time = "2026-09-21T17:57:10.713Z" }, + { url = "https://files.pythonhosted.org/packages/34/1d/da27bdd49b0cb84263288f5f48dbd915f2d17fd228fee2078ca607e80277/multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9", size = 311499, upload-time = "2026-09-21T17:57:12.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/36/ab0299a7abf53e38ff75d96c64480f1e2ff8edde985f93f7a1ad5e08bd94/multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94", size = 313553, upload-time = "2026-09-21T17:57:14.548Z" }, + { url = "https://files.pythonhosted.org/packages/5a/44/e21be702efc9c9ba097c8be874c68860b1cb6cf9c85611263039bf95b37e/multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45", size = 299898, upload-time = "2026-09-21T17:57:16.192Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e93bc0dc947c7f872e502cfaa7ad9d98f001e1c93cfa6f4940457a6e187a/multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8", size = 273054, upload-time = "2026-09-21T17:57:17.793Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/945b44493fbe7af26ffe1e43c506b376daeab070fc1a0923bef9bbf572eb/multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3", size = 294751, upload-time = "2026-09-21T17:57:19.631Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/0969a9b38b814e5fa3af1b0a57708c49b3d9b3d55c208bac6e98b87bb028/multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f", size = 284523, upload-time = "2026-09-21T17:57:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/c32ddf53234c228b1a374ddabca19f542ac7f32752f5155982abe2304833/multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709", size = 290275, upload-time = "2026-09-21T17:57:23.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/09/7a99a3daafd987330abb865a2d36e432fd8bf18e790ddda55c5de15eb98f/multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983", size = 303350, upload-time = "2026-09-21T17:57:25.086Z" }, + { url = "https://files.pythonhosted.org/packages/2e/09/7e6ebaecc4e226d7fe27973c271d58b75c14c24e84c23736d21dc39997dd/multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6", size = 272232, upload-time = "2026-09-21T17:57:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6e/420e9e879b21cbdb5036214ee238efabcfab7bf262d3fc124132050f79ff/multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b", size = 300525, upload-time = "2026-09-21T17:57:29.004Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/c6b0896850b3ceb190ca302760d63148843371f027c5dde3d1d732343a08/multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e", size = 295933, upload-time = "2026-09-21T17:57:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/f0ff3384a756227741c72f4ee94cadf1f9900df0ae961e48639f96115b1c/multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd", size = 57776, upload-time = "2026-09-21T17:57:32.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/87/013e1eed30ad46bec1eb68947bc9caf55ba2fd0d6aed04fe964e3b44c23b/multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9", size = 64881, upload-time = "2026-09-21T17:57:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/7f/95/64dd049bb4c53426e2fee7afb874daaa8b4426c41878785883a94511ecf8/multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3", size = 58582, upload-time = "2026-09-21T17:57:36.19Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2c/26bc1723592cfe965b575652a7749026474b0625b04b04564e1fba265dc5/multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1", size = 62339, upload-time = "2026-09-21T17:57:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c5/614d2eb9995232d066aae7f17d1d454060b6b5d882b22fbaf6150b05ef1d/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82", size = 55009, upload-time = "2026-09-21T17:57:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/f7179ecdf9cdd3581ace3292c3b883a1a500a374aa4e619bf6eb521c62e5/multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea", size = 56273, upload-time = "2026-09-21T17:57:41.544Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/c9dffcecccd1f8b9a58f960ec4eaecab62c4edb08d5bb984d4de67fe3fdb/multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c", size = 96390, upload-time = "2026-09-21T17:57:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3f/76542360e655cecdb7c158c7af88c69c4db0d2aac3421659d41214927229/multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08", size = 58900, upload-time = "2026-09-21T17:57:45.105Z" }, + { url = "https://files.pythonhosted.org/packages/04/15/f28c71af461dd4ff15653a165cfd8ddaf01f3387ff80d2dd0de057d527d0/multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b", size = 56540, upload-time = "2026-09-21T17:57:46.913Z" }, + { url = "https://files.pythonhosted.org/packages/19/de/5ee4050c3ddad88c605a89fc1cd18e89abd40a64f183e6fb5276b6eb7f52/multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919", size = 331818, upload-time = "2026-09-21T17:57:48.825Z" }, + { url = "https://files.pythonhosted.org/packages/70/e2/2d4e3c3ee70f583334ff236fe6a3babf7a05ce56169a15df65453af647c0/multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519", size = 329800, upload-time = "2026-09-21T17:57:50.91Z" }, + { url = "https://files.pythonhosted.org/packages/d6/14/f64bd05667343a7250cdd99f81bda2270e3959f00832441f10c7f904e1c1/multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9", size = 312048, upload-time = "2026-09-21T17:57:53.053Z" }, + { url = "https://files.pythonhosted.org/packages/64/bf/a91256ce00199e7544e3eda79c4e9544eb7388c38fa22f11638c2e357ccb/multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d", size = 343676, upload-time = "2026-09-21T17:57:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/628b978413518159228c2b2ec38645362ceb6427e935d83d716e26490a49/multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67", size = 347028, upload-time = "2026-09-21T17:57:57.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/5f9dc2b5ac7dd7a82458ae9cd283b99c185834f23442ace295362dcc5e45/multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e", size = 335670, upload-time = "2026-09-21T17:57:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6f319ee000f5ab3fa0055b194ad706fddd552c60c76a21d581c87db1d46c/multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca", size = 307804, upload-time = "2026-09-21T17:58:01.2Z" }, + { url = "https://files.pythonhosted.org/packages/3f/24/61be5fe1616d033532e8d63dc789c0f35b9793917834713eefe050589ba9/multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac", size = 323409, upload-time = "2026-09-21T17:58:03.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/eab8aaf59892169a0f4e8e78a5516a6caa6abd8e96e6410111258f4ef0ef/multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e", size = 315746, upload-time = "2026-09-21T17:58:05.372Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/d1e14bab80a74babf6e2ac971a878b84fdcf3a972f3ec3400ca12260fac5/multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e", size = 334529, upload-time = "2026-09-21T17:58:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/f5/50/a05b1d36f6d3363a5a334cebbf11624c9c7a69d66fd23345eb53b1d43b7e/multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c", size = 336455, upload-time = "2026-09-21T17:58:09.112Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0d/bf33cb097380252b08e1177b5ae0cab822c47ce80d236f3f724953c76721/multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f", size = 305865, upload-time = "2026-09-21T17:58:10.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/a2bd133391204b1cec5a5caf327dc9a06710439b7403220860a8fb164560/multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4", size = 337531, upload-time = "2026-09-21T17:58:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/c0972486f81a06c3bbb23063c85afaac53c51608f7e35bcb101a1006408b/multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d", size = 332464, upload-time = "2026-09-21T17:58:14.673Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/5cbbe852b45d8a7f0ab3ed042386e4894e463010eb230603596cb15d0fee/multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d", size = 51714, upload-time = "2026-09-21T17:58:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/d4/77/1fa118386deeae4e7a48870fa5cb073e748b0ac5265a6990651cefc48068/multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f", size = 58619, upload-time = "2026-09-21T17:58:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/51c45748acfce9cb69d89dcfcd09c874b385407fd6993e885d6905659865/multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f", size = 53969, upload-time = "2026-09-21T17:58:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/89d2d3dedfae2558853c1770c7cbcdfd5211eb4c4090b1e8cb570544a490/multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7", size = 107679, upload-time = "2026-09-21T17:58:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/c8/df/0c2a28870181c1762f90952b897f6db46c934ce073efe0442604cbc276db/multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68", size = 64753, upload-time = "2026-09-21T17:58:23.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/7e/9c6a3e7619459407d8958f7e56ab3dda7f0738f5aed9bb4ad48076e8de83/multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64", size = 62732, upload-time = "2026-09-21T17:58:25.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/eb/fd56c9d83ba0cdc3cf66f9acb278b24ef1f043b620b96d145685a3b57338/multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce", size = 288648, upload-time = "2026-09-21T17:58:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/51e79c2442b0f56ac4ac580108d713078b31297e028d468fc220971a305d/multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5", size = 304919, upload-time = "2026-09-21T17:58:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e4/774700cc5739353fe88a7204ed30e78a9403d98d4bb2496cc91f096d2e71/multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2", size = 289094, upload-time = "2026-09-21T17:58:30.938Z" }, + { url = "https://files.pythonhosted.org/packages/ca/41/0bec327bc3a8271825376b6d8a469d1f80e3ba1e2e730e019836e29663c3/multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1", size = 312007, upload-time = "2026-09-21T17:58:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/9906bf5f1fe20e8e755affccc1a69ecd8aebb079ac65386e5ffc8eefbe75/multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659", size = 314657, upload-time = "2026-09-21T17:58:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/cf6d665a45663bbe216420fe632da10e9f4f842b07ce02bc3ddf8eb882b8/multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20", size = 301563, upload-time = "2026-09-21T17:58:37.472Z" }, + { url = "https://files.pythonhosted.org/packages/97/4c/9f1b7945526905d58b4b2d422e2ea81326dda71f9bdc9c32c8002639234a/multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471", size = 278540, upload-time = "2026-09-21T17:58:39.551Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d9/133aa472fc1be181de29a0c2e1c4cd778280882f532b404395f646742e46/multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879", size = 294858, upload-time = "2026-09-21T17:58:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/bd/38/21f2305dd20ac5038fa581b5d7e0d8dbbd70dc6b50231c22ec173dacddda/multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3", size = 285461, upload-time = "2026-09-21T17:58:43.844Z" }, + { url = "https://files.pythonhosted.org/packages/ad/21/465a43c214d1d7e2d0c974e727d4b5935f8a0868838b5df9d0984c83c1dc/multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df", size = 288964, upload-time = "2026-09-21T17:58:46.16Z" }, + { url = "https://files.pythonhosted.org/packages/2d/58/72a3e8d56c1e05146d2d1841dbaacf8986375eded1e5a58d30eef191a580/multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c", size = 303924, upload-time = "2026-09-21T17:58:48.395Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b9/911084c04530a682a54536b4f738d3992a593b5ff6fea1d6a1f02f3acc6b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c", size = 278479, upload-time = "2026-09-21T17:58:50.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/eb/9a333346c02c928a9e82534638bce85697c10c46b4901fcd70af19357356/multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d", size = 301650, upload-time = "2026-09-21T17:58:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/1d936c4e45c8199db6082b6836e9182e31aa393de0de652637790831ca4b/multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1", size = 298024, upload-time = "2026-09-21T17:58:55.03Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0c/ca62ae2882b89a4cfeb0bbca873a4afdb5fc32994c4f8a37a7cb52ede7a0/multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1", size = 57810, upload-time = "2026-09-21T17:58:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/b2c2805c3eb6fa4cdf5512fb95ac6174dde71e34dd601c5be52f4df5dc82/multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec", size = 64984, upload-time = "2026-09-21T17:58:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/fd/34/03f1d204d698c408f4754c969882609b6b7a902c0e3d16a7033f219cdeda/multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3", size = 58742, upload-time = "2026-09-21T17:59:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" }, ] [[package]] @@ -4139,36 +4218,36 @@ sdist = { url = "https://files.pythonhosted.org/packages/a6/91/86a6eac449ddfae23 [[package]] name = "nh3" -version = "0.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, - { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, - { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, - { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, - { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, - { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, - { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, - { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, - { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, - { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, - { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, - { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, - { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, - { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, - { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, - { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, - { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, + { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" }, + { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" }, + { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, ] [[package]] @@ -4182,7 +4261,7 @@ wheels = [ [[package]] name = "oci" -version = "2.185.1" +version = "2.187.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -4196,9 +4275,9 @@ dependencies = [ { name = "pytz" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/47/9dbc5ac04d7f4230d49be16f4d724d44877834eefe77290e5215e17364ab/oci-2.185.1.tar.gz", hash = "sha256:b703be0a94da5a2b10158ced904d2eaa732c72f0a5d4582e476758fa4db1a8ea", size = 18139822, upload-time = "2026-09-01T05:11:14.816Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/01/d5f48c25ffb97345688f9474e088c0d06687225b4358b00ec4b3a5743c6f/oci-2.187.0.tar.gz", hash = "sha256:091913c264b98bac02b6caad3e4b005565e4788c3650f02647a3eda7f719df07", size = 18199653, upload-time = "2026-09-22T01:55:28.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/70/03c081f033f8bb1e73ed3098fa3340a2aafb97a0c96841a27188c6cc7ada/oci-2.185.1-py3-none-any.whl", hash = "sha256:b3028254c45e149806566b40fe981c32834a45160aad95570b4c2f9be3873578", size = 36808289, upload-time = "2026-09-01T05:11:07.385Z" }, + { url = "https://files.pythonhosted.org/packages/87/d8/111fca38d72ef3866e1da2262ce3476ba07941adb2c65c17f55e015117fe/oci-2.187.0-py3-none-any.whl", hash = "sha256:2ad560a4d404f835a50799d3ec1ff428d3b29f07ed65a27e07ca52e33afaad32", size = 37011917, upload-time = "2026-09-22T01:55:22.954Z" }, ] [[package]] @@ -4248,7 +4327,7 @@ dependencies = [ { name = "jmespath" }, { name = "jsonpatch" }, { name = "keystoneauth1", version = "5.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "keystoneauth1", version = "5.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "keystoneauth1", version = "5.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "munch" }, { name = "netifaces" }, { name = "os-service-types", version = "1.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -4264,14 +4343,14 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.44.0" +version = "1.45.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" }, ] [[package]] @@ -4282,8 +4361,8 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "pbr", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "pbr" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/62/31e39aa8f2ac5bff0b061ce053f0610c9fe659e12aeca20bfb26d1665024/os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575", size = 27476, upload-time = "2025-11-21T13:55:47.726Z" } wheels = [ @@ -4301,8 +4380,8 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "pbr", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "pbr" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/ae/fe7ac23155ae0b4b9779e06e9c5bb4070f2315dc4ca886a88fa3230d344b/os_service_types-1.9.0.tar.gz", hash = "sha256:1f2e5fb71d1f6f4ff31d8992674f2368465bc2f25cd94018015c3ddbfc5c617f", size = 28087, upload-time = "2026-08-04T13:07:43.171Z" } wheels = [ @@ -4365,11 +4444,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/d0/43/187e14a5e53a4541e [[package]] name = "platformdirs" -version = "4.11.3" +version = "4.11.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/72/2075f80b8de9992872715444e7088ec3b6eea2bc0e99521e1d6e562e382d/platformdirs-4.11.14.tar.gz", hash = "sha256:0e706ec0a73a4ec023d11496153a4b6607a672f4b5027c23c3aefa9c9288c523", size = 45301, upload-time = "2026-09-25T17:24:00.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, + { url = "https://files.pythonhosted.org/packages/dc/40/03f2c094178624d155b8d27a9b0c5ab1e39efa6cc9074b599e8c671b9db1/platformdirs-4.11.14-py3-none-any.whl", hash = "sha256:c1b1a1eab4f4d1b0eb81754593f1c38de88e84ebe6f93bf6714706a6759074b1", size = 26700, upload-time = "2026-09-25T17:23:59.443Z" }, ] [[package]] @@ -4383,166 +4462,183 @@ wheels = [ [[package]] name = "portalocker" -version = "4.2.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/92/d15473a60e4cb7bc450e10d3160d7d531d6fa8cb144e31395afd76cf4be2/portalocker-4.2.0.tar.gz", hash = "sha256:f4c5024903126159c4798e6aec6c32e30a0759db39bf795c0dbeb180a2e8c843", size = 259245, upload-time = "2026-08-22T00:22:15.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/cd/d2a23fc80c26f539ac77e7d61bd5e4af5d0b3a09a78ac4c716eead345129/portalocker-4.4.0.tar.gz", hash = "sha256:90c0df939d4ffba121f8e925bbf98ecea8b9381718666ab871a226938d2b63b2", size = 304906, upload-time = "2026-09-19T16:11:09.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/49/abd8a4154c911cc6c247edb7195d14846b614314c58ae09cf710d7d56098/portalocker-4.2.0-py3-none-any.whl", hash = "sha256:b33a8ce7d9b74922123a6fe64c2d785c015e5e9049ce7f1db5bd44e288731460", size = 128016, upload-time = "2026-08-22T00:22:14.254Z" }, + { url = "https://files.pythonhosted.org/packages/52/6c/07fa2775e87b6e241c2aca5cef79f65e450bc115de55f03cda6251f81690/portalocker-4.4.0-py3-none-any.whl", hash = "sha256:a8e99ea29bfb61766ee0b4008cbaf0651e8e050f7a2485ebf54316480e99226e", size = 129647, upload-time = "2026-09-19T16:11:07.377Z" }, ] [[package]] name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, - { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, - { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, - { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, - { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, - { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, - { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, - { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, - { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, - { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, - { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, - { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, - { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, - { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, - { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, - { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, - { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, - { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, - { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, - { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, - { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, - { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, - { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, - { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, - { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, - { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, - { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, - { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, - { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/997704118aea215cc5f65c277f5323657b4ba44c1f9a32fb11c8064e4997/propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680", size = 87225, upload-time = "2026-09-16T00:13:43.864Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/ceeca1762ed51230c08d557591404f844ecef5e294550136155572f7faae/propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e", size = 50809, upload-time = "2026-09-16T00:13:45.33Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9a/6be90814d8762952594a9e380161802541bff690f3a2e011dcc28ec193ce/propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2", size = 52552, upload-time = "2026-09-16T00:13:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/7fd3ffb5ca0e669a8fbf55d9fbb17c50ff5f44becfefcab27a9b9b67908f/propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4", size = 226804, upload-time = "2026-09-16T00:13:47.739Z" }, + { url = "https://files.pythonhosted.org/packages/74/87/a3e199c45b26587f073db655d19c30d2144b0f883827ba6ab77acf5c7d45/propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef", size = 234452, upload-time = "2026-09-16T00:13:48.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/7c/08c15c7df256f94a6b4563f74165c3242fb554cdd1909b661d093c31b976/propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97", size = 240399, upload-time = "2026-09-16T00:13:50.416Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/5adee15e12a7e314cece4543fbf295b0fe1b504dc78b32dff4c8eb1e29b0/propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2", size = 224251, upload-time = "2026-09-16T00:13:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/54bd510bb5d473edf66914602885226187218b4e3a5017e9dd80bceea41f/propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e", size = 202721, upload-time = "2026-09-16T00:13:53.524Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/a468700d0526dfb2ca6af5d790125de5894acc7d8ecd99243ecedfa4c4cc/propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874", size = 219148, upload-time = "2026-09-16T00:13:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/bf563e19ac9a5cc47113431337fdf2ee3573859f3f3e0a58e59833e9b75d/propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a", size = 211011, upload-time = "2026-09-16T00:13:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/0189b6537f4a8cd795e685a130a6f47aeda78a5b7b062574705cffc685ca/propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a", size = 228148, upload-time = "2026-09-16T00:13:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b7/59c16e245a549df202b4ebe2de91fa58a67dc4373df9840e372a64224dee/propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5", size = 201432, upload-time = "2026-09-16T00:13:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/41/0b/7b19eb20bb0b1f9476d08f6e387ae094dc6870fc06d459ea1f35f28b25d3/propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb", size = 228923, upload-time = "2026-09-16T00:14:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/b1a0bd47218216b935d019912d6fec1676ae7c07f15fa82bfb54b8d58976/propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f", size = 218327, upload-time = "2026-09-16T00:14:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/55/c0/51e5d1ad504e9529831606534c48e272db751072eee9ff07e4abe37e50bd/propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8", size = 43096, upload-time = "2026-09-16T00:14:03.427Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/0a0b4b1122f4ed5bcd1c626d910003cdf5282c2a12f8a1cfa17970b3ded2/propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745", size = 46589, upload-time = "2026-09-16T00:14:04.581Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/e71b24adc8ece61782ba3d6f3879e6a07fdb85bce21a9c529524184e3ec6/propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487", size = 43982, upload-time = "2026-09-16T00:14:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, + { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, + { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, + { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] [[package]] name = "proto-plus" -version = "1.28.3" +version = "1.28.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/6a/056256feb4bd000869aba5c16cf2aa911572ca2a2feb185f86e457b5171e/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9", size = 58051, upload-time = "2026-08-06T06:24:55.581Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281", size = 50795, upload-time = "2026-08-06T06:23:50.653Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, ] [[package]] name = "protobuf" -version = "7.36.0" +version = "7.36.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/e7/0553e21d25ca4d9f573135775348a372c3ec34a93a71d5f297c3bac38341/protobuf-7.36.0.tar.gz", hash = "sha256:e8e09cb0d794c6687926fa558a8a6e72aa10edb997d5ca61da0765f12a3e00ea", size = 510034, upload-time = "2026-08-20T16:34:01.071Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/89/5b8517baa72f84a67b8a307ba953c91057af618bf40bf676f3c03551f8f0/protobuf-7.36.2.tar.gz", hash = "sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb", size = 512737, upload-time = "2026-09-17T20:07:59.326Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ae/58e3ca96cb2e118cc546b677359b3c6659f79a140935c08dec94c7998585/protobuf-7.36.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:9103532dffd80c6fab7e50c65a31007680a06eb57537d437bb1b35812c138a37", size = 453256, upload-time = "2026-08-20T16:33:53.945Z" }, - { url = "https://files.pythonhosted.org/packages/f0/15/5162230af4912697f0fe406f6800f80760945babcff0e2c2fe6c84ef2d5d/protobuf-7.36.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:bf94a5917c71058262de683669bc0a797a7669d3de71f0b36d058e3194f47b44", size = 341436, upload-time = "2026-08-20T16:33:55.134Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/1670b2bfc9a45e807e520c3e9be36524db9ccc7dc05ea17af7681cabdc61/protobuf-7.36.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:3297e60abdff301e5f74393d87f6cc59dacab5f024a89548a6e8de1d26576b16", size = 354440, upload-time = "2026-08-20T16:33:56.077Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f8/bd5804695ba400e423c33fd4d9f58c28d86633d5ba1945c36ff3967d98cb/protobuf-7.36.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:70f5ec8eb0da81a44360c0dc0beac99a0d78071d21956a7076bae8bd2051841b", size = 340439, upload-time = "2026-08-20T16:33:56.992Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9f/acd02338235a3e7d03168c4303478347b7624fc8189ff4e7f0d2654bbe86/protobuf-7.36.0-cp310-abi3-win32.whl", hash = "sha256:7326fd717bdc419162a735938d89d4032332bcc3408804012b24ff3a37086071", size = 440216, upload-time = "2026-08-20T16:33:57.99Z" }, - { url = "https://files.pythonhosted.org/packages/0e/4e/12cb93270967a2affff5b3f720694700d4d87712a67afd05c8cb3f6fa52c/protobuf-7.36.0-cp310-abi3-win_amd64.whl", hash = "sha256:1781cc1de61249b750848029bca452c0a8b7e990080316b9bbc2518b2117b488", size = 453731, upload-time = "2026-08-20T16:33:58.951Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/629999e78d46c1115c11886d51c6bd68c17ce4a944f1ea3e153a91316a33/protobuf-7.36.0-py3-none-any.whl", hash = "sha256:53374d53fc29a67f7dbbf0ade47d7526a0f0137bf0f9c90e48d8a60790ef748c", size = 177024, upload-time = "2026-08-20T16:34:00.053Z" }, + { url = "https://files.pythonhosted.org/packages/32/72/98342feb672507c8f3a69e34b4fa8961f608edba5c1a48a6f47156d92cb5/protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e", size = 456039, upload-time = "2026-09-17T20:07:51.542Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ea/91fdf7c2b8bbd49cde056f00a9df6773532987e1c00fe2830b895af95c7e/protobuf-7.36.2-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e", size = 344219, upload-time = "2026-09-17T20:07:52.914Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/5fd5f8ece73fad885c5a09aa849b32d70472f954ba3a92d3bb5974ea953b/protobuf-7.36.2-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf", size = 357223, upload-time = "2026-09-17T20:07:53.985Z" }, + { url = "https://files.pythonhosted.org/packages/db/f3/3996583dd2906297a637af12114deddf7658af6e683fedb83be061983fb5/protobuf-7.36.2-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2", size = 343223, upload-time = "2026-09-17T20:07:54.931Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/dcc64f358fcb51811b58ae40b3d28f820725f116d86487cc20bd4b130701/protobuf-7.36.2-cp310-abi3-win32.whl", hash = "sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728", size = 442998, upload-time = "2026-09-17T20:07:55.826Z" }, + { url = "https://files.pythonhosted.org/packages/8a/55/b77bda4e5e5f5971fb51b07663694690e9afdb9402136c16a522bd621cad/protobuf-7.36.2-cp310-abi3-win_amd64.whl", hash = "sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353", size = 456514, upload-time = "2026-09-17T20:07:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/d52c7016b04b6c5108f26691f9d33ec82a9b65d041f1a9c771137693d618/protobuf-7.36.2-py3-none-any.whl", hash = "sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e", size = 179806, upload-time = "2026-09-17T20:07:58.211Z" }, ] [[package]] @@ -4640,7 +4736,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4648,125 +4744,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] @@ -4791,7 +4887,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "cffi", marker = "python_full_version < '3.11'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/ea/762d00f6f518423cd889e39b12028844cc95f91a6413cf7136e184864821/pygit2-1.18.2.tar.gz", hash = "sha256:eca87e0662c965715b7f13491d5e858df2c0908341dee9bde2bc03268e460f55", size = 797200, upload-time = "2025-08-16T13:52:36.853Z" } wheels = [ @@ -4835,7 +4931,7 @@ wheels = [ [[package]] name = "pygit2" -version = "1.20.0" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -4844,75 +4940,75 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/54/9273c78efd3d570091af585bdeb68a46089e80602dafe11989cca40c6d0f/pygit2-1.20.0.tar.gz", hash = "sha256:7253735629c22fff412a72c48c204b19c206fda9fcb01e51113d9689194cb1cf", size = 826041, upload-time = "2026-08-08T14:45:11.281Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ba/3dec5c58bc307558d7e846e2367658913fd7f49813f9c9afef12b05e3983/pygit2-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:97a903fdd3e6e99d22bac87f0aaf439ccb9ad86bb4c9e15655188e97c0e53513", size = 5727580, upload-time = "2026-08-08T14:43:36.628Z" }, - { url = "https://files.pythonhosted.org/packages/37/17/657bc991833d80321f882291408f89a8677701c93c47f48023a95381345d/pygit2-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0bba0fd1568d9ba2948cdfe0680019cb0b8c1d58e54825856b6e31ebf13a1398", size = 5715849, upload-time = "2026-08-08T14:43:38.348Z" }, - { url = "https://files.pythonhosted.org/packages/74/b3/3b02bc8feeec5ddefcd8782a27478b86f30a306916e356c7b7164c5836ef/pygit2-1.20.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a837854f1e7fc2c141eb2ac193a316e26812ca04ad4ed977ed9be92e598d27", size = 6069889, upload-time = "2026-08-08T14:43:39.956Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c8/145f9fa3a960cb1c4ef1f31dd6170c5e1cb870960ca238038213e0c515e3/pygit2-1.20.0-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d458c50de33f5d0f086122ecf87651aba4291a89d99d79ef46976ed9088cbd0b", size = 4667780, upload-time = "2026-08-08T14:43:41.528Z" }, - { url = "https://files.pythonhosted.org/packages/2a/18/ecbc0dcce05103dfc556f3b3c44d9fa4f5023ca32e977a5c152ce4de001a/pygit2-1.20.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45403d9f0c38df1ab2219d22e5e3fca6a7d40c27784a2a771a629a1c05a6a055", size = 5833427, upload-time = "2026-08-08T14:43:42.955Z" }, - { url = "https://files.pythonhosted.org/packages/45/1c/ac65749cdc2fa6efdc9f7d45e0b1cb1553df0155c9cf2921fe80161053db/pygit2-1.20.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e9b7f915c8a555f2300ec231652e91303b67654465f26669fb86a6c7b2d2dcc8", size = 5325995, upload-time = "2026-08-08T14:43:44.506Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8b/d92f8bbe1b90f703cc50ec12d4343c93f7e44656ef81ca0a2aae4b923ddf/pygit2-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ec2ade07eb56515f8fa4864efafb472ffac1715431338c985039273852aee8e", size = 6199586, upload-time = "2026-08-08T14:43:45.921Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7c/b5207badbc9dc61593280868a83949132eb17a4cd8a3503cf3d4173cfd0c/pygit2-1.20.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:db972efa0a0f3bd2f3f215cb5cbb3cca68b11fd638400167e0ec4c4d5726fb15", size = 4887211, upload-time = "2026-08-08T14:43:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/00f14abd34701bbf329e71e56e5fb70c2f0272d9ef572c9924f4b2fbed92/pygit2-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d6cfc3684ac7ca944e7172f0518adcbc93c1d7711705c4b36ef70218fc721ce", size = 5931583, upload-time = "2026-08-08T14:43:48.97Z" }, - { url = "https://files.pythonhosted.org/packages/54/07/c0701027b77555930913592b5a265eef09f7260221d4c6348a629972f0e3/pygit2-1.20.0-cp311-cp311-win32.whl", hash = "sha256:8c9073e38f216e937cfc69d8d93e97a59c59f06dfa67c557134b10f8927503d2", size = 1019027, upload-time = "2026-08-08T14:43:50.315Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5a/09e66f51a5147929fe4a5653e9e9f3820e91998820fd15837ecfdd98e4fd/pygit2-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:c4800ad2643e0cb540f802973866b16480d45da2c8237a56c0b9405460ea97e6", size = 1332836, upload-time = "2026-08-08T14:43:51.372Z" }, - { url = "https://files.pythonhosted.org/packages/89/c0/7241ced2014b485f6de3eb692236f0db8f6d92d79cbdcbff972d0b78614a/pygit2-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:83f1ccb0455e801b70dc13b4fec6dcc5ed0412ab7a38b986e0f559c9ed0bf7e2", size = 1043630, upload-time = "2026-08-08T14:43:52.8Z" }, - { url = "https://files.pythonhosted.org/packages/32/4a/20433a321dc495231f6c017db880dfb6e0d5436780a699ce5e6ac5c1fead/pygit2-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dec857dcd64cfb3f52d2e79faa53ff9fc34a6db2ed5d3f865bf5a7239177de56", size = 5725662, upload-time = "2026-08-08T14:43:54.133Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b7/add39585242cd7f24701e4899e6cff73cc8aa64dc9c945497071043f39cd/pygit2-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9374c6f8d8ee0ba01a58238416ed2b24d4c154a8823ebc6b638a7ee4a480415", size = 5716598, upload-time = "2026-08-08T14:43:56.171Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b8/6039aef21a18da62dd41c0457af47c6d84e24845b1cc938661ec56446a55/pygit2-1.20.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62bd32dcfc1982e1cb2218ca38c14c511caf970ec64ae2d39e6aa0d9d62dc4f0", size = 6070500, upload-time = "2026-08-08T14:43:57.701Z" }, - { url = "https://files.pythonhosted.org/packages/26/0b/8e9852340ddcaea2f4f28f4d4f6324254c75d845654aae61f67b7b2b084b/pygit2-1.20.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9a540410f9f893c5e68e221d1df21319fe5add04e45d51b2329ce0fa14516b7", size = 4667205, upload-time = "2026-08-08T14:43:59.178Z" }, - { url = "https://files.pythonhosted.org/packages/33/26/bd9b33dbd8535b4f667f7cac56a15740fa5039c8ceb735fda1cd0d2d3b65/pygit2-1.20.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2be2e29283268a2842342c7d4e206e1d8326dfc71598743fa8ef61ef6d744d5", size = 5838298, upload-time = "2026-08-08T14:44:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ce/7a7c5007c3b1e7082503b23a51f1329f42525cda6531630623fb52ba8390/pygit2-1.20.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8119ef9b00587f243ee5462dfe5dc71aad600cf1b25b268bffa67065d3412439", size = 5327204, upload-time = "2026-08-08T14:44:02.11Z" }, - { url = "https://files.pythonhosted.org/packages/33/61/fa9b9c75c4a87d5877179c13ea26e958f2ea5bb6a42f5228615bf26e4010/pygit2-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7150878d113dee6291ca2d17876729b739e762d1585d90459aa61b34bd662de7", size = 6201154, upload-time = "2026-08-08T14:44:03.481Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e2/3ba5a1d42e3334c93387eb4d01365762f735c25655e9907c23d58e110cef/pygit2-1.20.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a79cf9048797997cc3610726b7b2827d6459961208553b187522786ab5ffc0f2", size = 4886640, upload-time = "2026-08-08T14:44:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/23/6b/bba0edae78e97e220908c33a8bf1ef5418bc558e3c657ec0d29f18e77304/pygit2-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97619aa269f31f2b94e83078b5c6df4e7a0cc9bdfe5b672378c10ed09d50c360", size = 5936908, upload-time = "2026-08-08T14:44:06.544Z" }, - { url = "https://files.pythonhosted.org/packages/09/9b/22149d9b5059ad47b21bf9c7791fc59ce2226aafb95dbfe0e6e7fa54af89/pygit2-1.20.0-cp312-cp312-win32.whl", hash = "sha256:d49d7bb4987fabece611f4f873bfe490e574f0bb5bb4fafe557d4d504efe3026", size = 1019713, upload-time = "2026-08-08T14:44:07.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/8c9fb460aa70e34681e26d1b314dc2ee6a7de556941d20d6fec3485d0377/pygit2-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c16126d27218e7b5d059e5072b15819261ec3ad515ff45ed611fa1687993c25", size = 1333225, upload-time = "2026-08-08T14:44:09.124Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a3/8ca3be722208fd764364feabe5c2a9186a96dcc2be73feb7f7bfce4dda5b/pygit2-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:3f7b97f577d7d15d59fbef8e3f6f6943bdc5e315cbf6d4c0478984858e55b0ea", size = 1043775, upload-time = "2026-08-08T14:44:10.157Z" }, - { url = "https://files.pythonhosted.org/packages/f9/52/4247a6180b6a4100a7232404a97cad74a62f19942c405383f1d13c361168/pygit2-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9217790892f6c9faf38737a078ac36203bd082b226e0b0ba68d8930f4ba7737b", size = 5725672, upload-time = "2026-08-08T14:44:11.347Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/e28c2f0bc3d753158e93900bf0e236038b79f2d20a09be8952f1c4c1e692/pygit2-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c21715780541121c9784a7e4372c32b440b3ac67452b5364af738f5f4b82f8e1", size = 5716639, upload-time = "2026-08-08T14:44:13.068Z" }, - { url = "https://files.pythonhosted.org/packages/6f/76/ca1ac1f568e7a326a01b6b1e09960f4e09a7f1a06c6d93b0ffdb28b51dd3/pygit2-1.20.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23a1879065591b65b26f58048450629c2cdbdaa2a65328eee27d908614b963db", size = 6071276, upload-time = "2026-08-08T14:44:14.639Z" }, - { url = "https://files.pythonhosted.org/packages/af/88/5d56e8d5bbd75e3929afe177755bb7c28cf4e8bf1cb05eb3713769b3e75f/pygit2-1.20.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e51e9a9a4aa15bd570d57c5febc0142b86a19c4f2b579e9a33d6c170189ff9b", size = 4667815, upload-time = "2026-08-08T14:44:16.291Z" }, - { url = "https://files.pythonhosted.org/packages/5c/84/8ed86da20b9d451b2be4a916f9ede203fbfdd6aa40c5e54ffadeeb4544af/pygit2-1.20.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c7fba542df37ef208dba315579df6ea62c841ed60101154e15e811480f3f5ae", size = 5839457, upload-time = "2026-08-08T14:44:17.708Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7c/c29b37d10614ee0787e3fdaa79664c8bb68f38587ad75f8f84122021f3c4/pygit2-1.20.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4600c5cf14895bd7a4ffac14932969c02b6fd7ce2a047b004f3397bdc040425c", size = 5327439, upload-time = "2026-08-08T14:44:19.089Z" }, - { url = "https://files.pythonhosted.org/packages/aa/04/f462bd2bf8acd34859418e72de805b92af7d216d37e5ddbfd09a01284dc9/pygit2-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229bbd20bde6a9fcceee732ca4bc30360d375b0db439e19c6b9a0e1d3bb4311e", size = 6202569, upload-time = "2026-08-08T14:44:20.579Z" }, - { url = "https://files.pythonhosted.org/packages/16/47/8805d05d6a1d1768f5ccfc2a87d7ea33c31ba99d7ab2762b838eccdda922/pygit2-1.20.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:223be35205f6dd4ed9fd35ae9feca59fb1a98913de090d8716c5830495e31901", size = 4888440, upload-time = "2026-08-08T14:44:21.93Z" }, - { url = "https://files.pythonhosted.org/packages/f2/59/8ef9d3c585d1d37acb236e2fc637905da931c9321dced6ce9ee3754a9145/pygit2-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:46b6bac662c55cfbdc137f131050317d56e508a4506ab2e8ac01518f33a29bcd", size = 5938354, upload-time = "2026-08-08T14:44:23.287Z" }, - { url = "https://files.pythonhosted.org/packages/68/63/97aff45f675521f1a0397e93e7b8f623b86ef79c70567ea8bbfc0bee6ccd/pygit2-1.20.0-cp313-cp313-win32.whl", hash = "sha256:a455a1d9714b49ba36fc45985d58debe3d6d07fbaa187cd217f79713ae1d1af7", size = 1019675, upload-time = "2026-08-08T14:44:25.099Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/428fab56d8df919bcd735aa9ef0ac9100ab47b9ed9d9aa4cd0a686da9b5d/pygit2-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:451bfe3ec1719419157f49423d1a13a90788df78d2e97fe53efd2c872dbdf8a1", size = 1333178, upload-time = "2026-08-08T14:44:26.254Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f3/51e78c77badab4b73434749bc19ba1f406c191e0911da6a051b803c3445e/pygit2-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9f715e5ddef14eb4ba344b1deca8a82a2015acb01f6aa0a79eaea122dfc9384", size = 1043747, upload-time = "2026-08-08T14:44:27.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/99/c6b55c5f695b84921afdc4b52ac14191e4b8006e18220f09d32d63d15c1e/pygit2-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79dece6a15fee4e0c1fc9231b3156327933adbdd6d21c2070e3c06c26add3d73", size = 5728008, upload-time = "2026-08-08T14:44:28.757Z" }, - { url = "https://files.pythonhosted.org/packages/ca/43/a6639265efc0ce9cdbf9943cac9ab03d74f26128882059495193c7d0652f/pygit2-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:15907f5b6e28dafd54062e41e9375cb4dc2f9cc37ffad58b0195dfeab58b3325", size = 5716504, upload-time = "2026-08-08T14:44:30.193Z" }, - { url = "https://files.pythonhosted.org/packages/66/04/6c9b77b553e55bfc104540969df57e08c10ca7d91e26c035a7d58f9a681d/pygit2-1.20.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68b4cc2a442b59202c038214aa93ea9db8bb222429552e545afe0a98a5ccb004", size = 6073519, upload-time = "2026-08-08T14:44:31.745Z" }, - { url = "https://files.pythonhosted.org/packages/41/c1/9507ccfaac42424d70b6acea6b03e2645e55d6cc57efbba458d718ed117e/pygit2-1.20.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9c63594002feaa4b406dd3b133b4d40a1c53dd285f9bbf3156af89647f2ddf2", size = 4672126, upload-time = "2026-08-08T14:44:33.154Z" }, - { url = "https://files.pythonhosted.org/packages/49/0b/eea71c54f11e943a3c6493df26db59fc4fe859bf701bb366992ba241ca76/pygit2-1.20.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b99f40e2b3ea98e8ff905958db279d21e735d2fb5888818b359480f0883c2d", size = 5841077, upload-time = "2026-08-08T14:44:34.499Z" }, - { url = "https://files.pythonhosted.org/packages/85/a6/46786c212302b4bc10a26b64640a7419db17aed48e420b191a0a76abfb9c/pygit2-1.20.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf93fd9dea38087f6642837bfeb6db722b661ea9dd90b604d06810063ff030e6", size = 5329043, upload-time = "2026-08-08T14:44:35.806Z" }, - { url = "https://files.pythonhosted.org/packages/65/4f/b0159fd6d4efd997ff5f3eb8b3340b7ac797b98f8c06f51aaac19c0b444d/pygit2-1.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:115580869b6c78bbf07b24f389c8a146c6f080e55121ada2506b3ca29710dead", size = 6203519, upload-time = "2026-08-08T14:44:37.722Z" }, - { url = "https://files.pythonhosted.org/packages/a5/10/797f5583a85b013a0f216ce3c88f6f5556f9fa4b6bcc5d1de297e6e21d6c/pygit2-1.20.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37e0db3a58603531d6ceef3b36b0bab645a2a81b91c1b8e0e1111f34d67fa61d", size = 4889896, upload-time = "2026-08-08T14:44:39.097Z" }, - { url = "https://files.pythonhosted.org/packages/41/96/cb1e48a08bd7030dbb4455a1c3f940bcdfc2943e24bbac3cfe1150753652/pygit2-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93ed5d0c305c265b97337844b2cecc8075ff05f657475ffcefbe985de8b04134", size = 5939118, upload-time = "2026-08-08T14:44:40.445Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e1/c7d2683de4c7fb59b7f6f3d2c6dea638fb72c8c45655e1639517c7c8a8d2/pygit2-1.20.0-cp314-cp314-win32.whl", hash = "sha256:c7ec615560faf3a4f868263df680954bf3deb74cefd24be63a2caad4ffadf6f9", size = 1044051, upload-time = "2026-08-08T14:44:41.801Z" }, - { url = "https://files.pythonhosted.org/packages/a4/52/2a903622fa9599fc0e0d68a7614513ba26248a89590042ee7752d58c0d35/pygit2-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:4ae51387c4016966703f990fad2a1adf60f594239d8a0f5b308ec82921e3b582", size = 1368061, upload-time = "2026-08-08T14:44:43.145Z" }, - { url = "https://files.pythonhosted.org/packages/a4/bd/e05a00751c65745383fadddaea02d49d81a4bbff2d7d1d7a745c7763f67c/pygit2-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:055057b36ce141ccc6ce600475c3f8ce0edadad45c78b480cdcb61da29f7ecf7", size = 1074468, upload-time = "2026-08-08T14:44:44.609Z" }, - { url = "https://files.pythonhosted.org/packages/13/a1/4ba9ac3ce81f02d2f089c3c878282767d06eb97716834432917aa676cd58/pygit2-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:00415a2aad3a4e1eeeb1da5c0d805e992971e849a1a7d9f604c8e0381dff70fa", size = 5731464, upload-time = "2026-08-08T14:44:45.997Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/231bdc55a8243c44e6cab4772b863842abba73752258d8adf93ca12d2d5a/pygit2-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4207f901c377410f3d85df259d32903af1523258150968a1add805f40daad8bc", size = 5719730, upload-time = "2026-08-08T14:44:47.525Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bc/8688683a38f3b250ccc80b64f4a157a9cfb61dd836989b78ba923379a410/pygit2-1.20.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4cf13d0b12fc1df25953a23c3112f4cb8abed1802b0479b317086f79f5d1511", size = 6134328, upload-time = "2026-08-08T14:44:48.924Z" }, - { url = "https://files.pythonhosted.org/packages/81/1a/6ecf1236d9ba766e97797735d6d584b3713dd1ef03628b39c87ad231b90e/pygit2-1.20.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3b2db875ff92b288bb23cdb4bd398b045869e98c8dde55ba49d9abf264f19fc", size = 4730024, upload-time = "2026-08-08T14:44:50.259Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/304e83eb83da4b84d90168f003f9ae6761871f97285fe4352760abc098e6/pygit2-1.20.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fb2becd2cee9b610da613902aeca5426ac8c4a09e16344e1f75802f2ffe2bf3", size = 5893950, upload-time = "2026-08-08T14:44:51.797Z" }, - { url = "https://files.pythonhosted.org/packages/2e/11/e64b6414998dbcb336a5523e1e222e7656f5d679f7da7d797962fce57ede/pygit2-1.20.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cbbe502c735ee630be31be1c6e20f9ef35f980c492301c77969ce3d883242ffc", size = 5379546, upload-time = "2026-08-08T14:44:53.392Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ba/22ae0fe087d1eef7ab694882a3ab5e0c4cafac63ab2dc8e9c5716c5d4d3b/pygit2-1.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:254e0b61201c78b1cabe87d06d60891676311da07b5393a04e0ac813c130d583", size = 6261117, upload-time = "2026-08-08T14:44:55.394Z" }, - { url = "https://files.pythonhosted.org/packages/e0/20/71b6064e532b8071035f40a1c727040861eaecfe8b04ecfa0a998fcfb7c0/pygit2-1.20.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7cb0607755a251ca5ab942f1f45796dddb03b8499770f8c97d8de5f3616233ee", size = 4938423, upload-time = "2026-08-08T14:44:57.237Z" }, - { url = "https://files.pythonhosted.org/packages/51/bb/3751758fd0a2e52881e3b760376f9a848483bbf80db01f13fcea556c293e/pygit2-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c23e5405d6837760d3de58635492221067ee2d89c000c7b53e39e679b378ee1", size = 5990631, upload-time = "2026-08-08T14:44:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/50/19/dc0af298676f0dc1c1affedef0fdd49c3e831b71442c63f4047d3da81f4b/pygit2-1.20.0-cp314-cp314t-win32.whl", hash = "sha256:20dd3576c05cc94dd6ad6650d0a937593de6115bcbfeba37414dd57fa33c9b8a", size = 1047236, upload-time = "2026-08-08T14:45:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2f/cd193585ba738814cab54f6b99faa12e8449b5706b24d242d1c7dc785b0d/pygit2-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:82006722c4aacd4f5e4c93cd8914d1dd1521ec77a71c37f50ac58bc4f6b563f4", size = 1371325, upload-time = "2026-08-08T14:45:01.439Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b8/c0e5ae3702f46a36fa3bd914853d52c534968e0417a2e5775f439a921317/pygit2-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3831bf91550c487540f3e2d5cea211689b8e65b4589e812d3c335cb5500a5eae", size = 1075577, upload-time = "2026-08-08T14:45:02.598Z" }, - { url = "https://files.pythonhosted.org/packages/3f/9e/7388a45ad7a1ab0f2e68d43a1117b3dc0526fb383e0623da0ce402b15555/pygit2-1.20.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e6432cf4eb368366affb1bce4eaac750f03ee440defe5ee185557ba7e656909a", size = 5666736, upload-time = "2026-08-08T14:45:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/68/15/865f9b69a98daf0d218dafc2946c074889cd5291e3cd7dba3fa2af2b11da/pygit2-1.20.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aff3a6312006e668ce35983ef3c8d3c27115a37ce9ceb6ce316422f25d44b01d", size = 5664511, upload-time = "2026-08-08T14:45:05.361Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a4/ed18018054b88e378918088943ddc87eace2794ed058e66ee003dbb5779d/pygit2-1.20.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8108f420748549807603e5463f46df123844a7b5c0123d990d1107f308a3171f", size = 5577852, upload-time = "2026-08-08T14:45:06.776Z" }, - { url = "https://files.pythonhosted.org/packages/63/d0/b769009606f3d3adb7eaed9cf889e40dc5c6b03be2896fc2c924c3a886bf/pygit2-1.20.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de6729361800d312f9062c53a218f141af8909bad44c4d0064995728a284e8ae", size = 5333941, upload-time = "2026-08-08T14:45:08.433Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e6/dc41a33491517159e72ed197e60b50f74fd4f22903308f89dad815993205/pygit2-1.20.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e063d809e5ae8624a3c29a9743417670f2f57ab27fcf300f4e35575f9f30be79", size = 1300896, upload-time = "2026-08-08T14:45:09.999Z" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/11/592cc7854795830a7257ab6025a1fc803b58b0e7bf7d31f619bc7288ed4d/pygit2-1.20.1.tar.gz", hash = "sha256:36dff84d237f2b8f18b0b146d6e7c3f99a7bce2da98cc4103a14387f53319f95", size = 888235, upload-time = "2026-09-12T10:33:12.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3d/59bb36ab42616dc7a82c1ed440653031caae4c70a3973dfb6c56b572e169/pygit2-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46664438cc9aadb342df445be15d96c4bea77544ac641ab79d2977dccd0f0948", size = 3254898, upload-time = "2026-09-12T10:31:26.202Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/306810966da1ff7e151fdb25276d75972a3a110fe0c3dbbb2e668a6d7b9f/pygit2-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd18be3dcb5d0d1723f9294748f681d7206ad5e54202e862239f07f10e006803", size = 2740165, upload-time = "2026-09-12T10:31:28.156Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f3/72bfe9b102c94bd96a72c77c5e5423bd1e9e982b4d8498f133c2b0af6122/pygit2-1.20.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daa99ea66858b50ad1f6eab699d9cfe3b8c2c43e17296a5c9af6c71fa8d96576", size = 6076547, upload-time = "2026-09-12T10:31:29.814Z" }, + { url = "https://files.pythonhosted.org/packages/74/d8/c5d58bdc2965995dd2280dc9f526706b266982c977f0633c221b3376be06/pygit2-1.20.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9eef7be5132651da77b6278330cde2b776f8503e1a7a9058c790b0cb62dfbb5e", size = 4674686, upload-time = "2026-09-12T10:31:31.402Z" }, + { url = "https://files.pythonhosted.org/packages/6e/24/66c81f724a46e5c79dc086bd4e214843baf3642d2de82fa7cd6eb287b137/pygit2-1.20.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f6f393dd052e1f635ea9f6b7d825cd9c8cd1c9462f68a197eab086657c7cf65", size = 5839731, upload-time = "2026-09-12T10:31:32.952Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/76eed7efe549037605eb7604111bbe94f47e7d0849b1977c6c13f3fa2cab/pygit2-1.20.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ee44842ec283a6d0d382a9df17ae757c1c2f975fa2e39b37bceaa5e3528dca29", size = 5333223, upload-time = "2026-09-12T10:31:34.433Z" }, + { url = "https://files.pythonhosted.org/packages/47/ce/5b07b8a5d3c949be8ee5403a6e9117a2d785ce49f4b6f48c37f7b0c32901/pygit2-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:af88e5e152c35919fc18de2c588434919e491a387316e9232faa9c615aa88904", size = 6206777, upload-time = "2026-09-12T10:31:36.622Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/b372594e3f0602c44c9653e2986e1d533a632efe337f6737bd3601fe5ac1/pygit2-1.20.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:da44497239265ff0bf6b706ca71578399254a954aa4d8eb780cfab075893419e", size = 4895009, upload-time = "2026-09-12T10:31:38.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/b94f0fc1cd3d537c89c05f0b9db06d465cab92f68dbde9e76861eb69bdd1/pygit2-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e387b02ab5ef01f03ba84ac0a300c3b13cf427d9063dba0fb4a66a6cf23783e0", size = 5937978, upload-time = "2026-09-12T10:31:39.683Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5a/a22a72b9b955c29b7a6b640608ed62d4f4da4be8dca79d34be8f704783a3/pygit2-1.20.1-cp311-cp311-win32.whl", hash = "sha256:2b321fd4e29c4ce8b46108730ef3e145cd3b7de5fd118cac8cabcc5d9ae989f5", size = 1020160, upload-time = "2026-09-12T10:31:41.532Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/22ee783378e5a3ea4196b97657a193e132e021e9b8ac14175558337ddb1d/pygit2-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:4c5d154468206cf617340e7c9e96b53fcfee0c180cd38834c1dcdc5b0d0be8af", size = 1334099, upload-time = "2026-09-12T10:31:42.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/25/16045b6c2cc7c33122f35e2b47c60f0ccd1d8be2eb41914b8bacdbe096ad/pygit2-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:72a4b9efdb1bbeb97ba466ac1c02f2bf7ed1edffd96d02a146017228eac68191", size = 1045192, upload-time = "2026-09-12T10:31:43.994Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dc/ff53b26831e7baba14ee85e581737518e0927fc9cbde54c3ce1f21b19277/pygit2-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a0a1353e1e0074bc79f506c60b63dd6b59ff60a1f21570cb9721ce73ac3b8262", size = 3252807, upload-time = "2026-09-12T10:31:45.418Z" }, + { url = "https://files.pythonhosted.org/packages/26/74/384f87c037be551d3d5ae81ac183f06acdaded7f9ff5173a49deebb940ba/pygit2-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8537a48ab25338f38d5707e8d34d8817eb7cc492fe551ef0219980fe6f77cd8d", size = 2740993, upload-time = "2026-09-12T10:31:47.379Z" }, + { url = "https://files.pythonhosted.org/packages/70/c0/9602ec6732afabf2954c6ea620530e76f5b6c01de6a3ead24d25654a1bfd/pygit2-1.20.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09a132454dbc97f80fb8155fe5d2cc40b1924827c42e8c741b9950af6a509ef", size = 6077979, upload-time = "2026-09-12T10:31:49.151Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d9/c719857971470edcc895991657b66da697f0eef3bb34f3b417ca956a9fec/pygit2-1.20.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d60a644d1280210f88e00ebe9929212eb74b0e32260c61c22c0d148fe8d0afd", size = 4674943, upload-time = "2026-09-12T10:31:50.698Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c1/2a5c1ab50e7e4c59dbced3ee81878aab0510fcec724583f4b5456a2d702a/pygit2-1.20.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39724d0d4f922058e1105c6a084f5c801eef6b7991033e6cb8d3efd174ef5338", size = 5845519, upload-time = "2026-09-12T10:31:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/65/a6/3b3e3e6e7b9f0b30f3d6e0387aa2ce7bb74ca534563a6d311fef3acef122/pygit2-1.20.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:befdb91f1d5f09981289d9785948a4f902cc3a961904bf9ae40010ef71fd51b4", size = 5334295, upload-time = "2026-09-12T10:31:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/35/a0/20c4fb35ab6b47b42da8bb30048a38b213af05b2d37a58754de402816fc1/pygit2-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f25df036a3ea4fcaa5051a1cc3adc1ed62e328a3a68b139ceb41b344882c6ea8", size = 6209345, upload-time = "2026-09-12T10:31:56.484Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/f90903e4b0a509875410206fbba6c1f9939dc314ea48548e4b75f3ef6abe/pygit2-1.20.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:009b2b5d2eb01f5da2d6dcdd51acf67478dcf953799c0d1f425b04e60f4fd3d8", size = 4895904, upload-time = "2026-09-12T10:31:58.582Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7f/2bc0bd2f20a813aca967d7e29bad8079d9ec1c99a2c0801b2e7c54c9cd5b/pygit2-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:579ab2893420983662bdd1a6870f889e03674fb9c9dc314075083b7f741d3bf5", size = 5944462, upload-time = "2026-09-12T10:32:00.176Z" }, + { url = "https://files.pythonhosted.org/packages/2b/55/956a09626492d9806b81636d77c39da633c97f48c674629b48e009b5c6c9/pygit2-1.20.1-cp312-cp312-win32.whl", hash = "sha256:532a63e6a6f2457465d1c1497a3dabc2676daf17d7c8d919246cfc99137e3e57", size = 1020954, upload-time = "2026-09-12T10:32:01.738Z" }, + { url = "https://files.pythonhosted.org/packages/11/87/c0604188f5e3f9e6680ae510c8b51db1989fb5a04c6e1268468a8600ef1e/pygit2-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:0bab03e4879ea55fd9c7b16c2d28d8023484c82e1d145fad67b1ac0efca596f2", size = 1334612, upload-time = "2026-09-12T10:32:03.086Z" }, + { url = "https://files.pythonhosted.org/packages/c8/1b/c459d5de701a4b3a07cb9ec8282de0c60e5b4eae3f349c4339946fbf5906/pygit2-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:38e663e69224d02611c7293d9bd633a2237560d730db7c3d9fd3662a468105ef", size = 1045384, upload-time = "2026-09-12T10:32:04.643Z" }, + { url = "https://files.pythonhosted.org/packages/68/33/33981faa8cf2dba822cd2722c3f0f8e3c2a12de184870f27c70e5b3cdd7c/pygit2-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:21adc71ee1ac877b00118c21d5f20150c90443e04b60e4da7e9db8504aaf048b", size = 3252831, upload-time = "2026-09-12T10:32:06.51Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/03cc1329295f144ab05bd0f4f8d1b16688e5e58e52010ac8be386809aed8/pygit2-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e60f5d8a01593d8d51c97325a7b6b5b1f644fccef1f54c1b0a6d47f11ab359c1", size = 2740977, upload-time = "2026-09-12T10:32:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/86/b7/8f054acfe48e7d9db5c2d1991b0015bbf2e483725205b42201cf590ebf1a/pygit2-1.20.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e958111749908c4f1989e33f3a98754eda56b3279e56bfab6d6fb513a7ea688c", size = 6078889, upload-time = "2026-09-12T10:32:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/70/c5/66f6b74f6945213a840b90fe9087f05a124dcd3b8cff0ce77bad11ecc5f3/pygit2-1.20.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:96f45b908d3daaea084f2ed659b1227a1727691a5a980a9bec3e37541afc1f22", size = 4675556, upload-time = "2026-09-12T10:32:11.914Z" }, + { url = "https://files.pythonhosted.org/packages/47/f2/148f971a80fa344f56674173e1c3f53c769da32f35a04214d805e8e4ebe0/pygit2-1.20.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f65c55b5217dd2cea0fefe287624bb9522266d984a06d1e87c1879cf6bd7585", size = 5846364, upload-time = "2026-09-12T10:32:13.44Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9f/9b12108a6f3bf9171c575cd8eadabc4bbf3714d9a3c7dd7c83da4e85b9d7/pygit2-1.20.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a1201c416db8e9ad572a389299c2db9df36d613676599f0984b78446db55437", size = 5335291, upload-time = "2026-09-12T10:32:14.945Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c0/4feabd87ca7bed628fb0c1b6d11d8089a78206f7faf85fa722852f81df47/pygit2-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:083df8b7b113afe3ceabdf17be8e7e4156f2938e22d2b3d17c96965568eea1b7", size = 6210670, upload-time = "2026-09-12T10:32:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/23/28/2d5d296120922aa8ba7791ecdcef8b7b90ad1506cdf8f503bda7189728ac/pygit2-1.20.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2e8a64a50f8ad839acbf069f2552046bcafb01ccbe632dcb64cb29417f870ed1", size = 4896954, upload-time = "2026-09-12T10:32:18.426Z" }, + { url = "https://files.pythonhosted.org/packages/67/73/fe01662f6da9c163d9c74033a23575080925c67f1b16eacdc9203ba0d928/pygit2-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb369a00ebb1eb513c5219d9975bbd7d6a0e9b551c5440299142a21354b7d121", size = 5945997, upload-time = "2026-09-12T10:32:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/31/f4/ea4a51410b91a1aedf1ff70f75e5a2ee4c0256b9f40263e13ad44c2b9403/pygit2-1.20.1-cp313-cp313-win32.whl", hash = "sha256:2eef49c2d0f1aa089c60b92f2b20604e3f27991bd1ceb8a8a51fb13075ce8427", size = 1020921, upload-time = "2026-09-12T10:32:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/a4/f1fefa5b2abbe95783ae265f17ee1da92bc974b95473a88cdb95cf7b7c5f/pygit2-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:5e4d6e37db59712e3f2148c33464536faf32bc863d283d97ebd280632ed5f138", size = 1334589, upload-time = "2026-09-12T10:32:23.482Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/13aa2445c32d26a92517cfd9fcc138cfc1b90b2901f4e4674bc7b1a6c9a5/pygit2-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:fe108609d988fee5bab198f2ad2cbbe9b5eb08c64919c0f68fcdb7adf6d5f3f0", size = 1045379, upload-time = "2026-09-12T10:32:24.92Z" }, + { url = "https://files.pythonhosted.org/packages/38/80/d8631f8f097a18702aef0d4e5da245750aee3913aeeeda33f02c0440b681/pygit2-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ceaa949c826975addc1cfb7d9b487714e9fded04cca9dcf9b7a844ae2da8657b", size = 3255211, upload-time = "2026-09-12T10:32:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/05/4b/a769e5bc68af8a4ad515b06cd7b5bc050469b7132de483d119ae0efe8242/pygit2-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c69da2cd18366c2b9827d9a9c7ebb9dc593fea5defbbc9b7704c8a6222a7d56", size = 2740907, upload-time = "2026-09-12T10:32:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/99c223eebe0d8ad5310648deea8a7fe5dbbcd0aa1111ee412cfc8accd028/pygit2-1.20.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d507bf62f5d447e382667411972921e0afe923e9567f3476a1f7b73db8bf49b", size = 6080633, upload-time = "2026-09-12T10:32:29.497Z" }, + { url = "https://files.pythonhosted.org/packages/39/8c/b8f5fb49274d8fcbc98d10e7879b5adc3d44811f2f4046754f6627c337ac/pygit2-1.20.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc36d68a9fc632ba8cf54dc2823aa03bee966d2e787aaed35fff606996519c0", size = 4679280, upload-time = "2026-09-12T10:32:31.218Z" }, + { url = "https://files.pythonhosted.org/packages/b6/01/f6e3c18ad9dabeb7302575b1174864fd90842bd4afedb3f5bdd926047ff4/pygit2-1.20.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:860c971fd53a9f14713a51b6343827b82d2b7dc7955e8c28c81ca3c90033b6a2", size = 5848124, upload-time = "2026-09-12T10:32:32.748Z" }, + { url = "https://files.pythonhosted.org/packages/ae/40/0d784566e7d7ddfd240c899947b5c384dcd6a2895285a71006958ef40317/pygit2-1.20.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:523a1571a55e4dbb33bd052ed72132ffe02e204fab5229b840ffadb9ec62e671", size = 5336396, upload-time = "2026-09-12T10:32:34.715Z" }, + { url = "https://files.pythonhosted.org/packages/39/5d/ce04fb8420d6e2809067bff29d9866607616428ccae5d00fe2cd15665bfa/pygit2-1.20.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e40c7221c781a5421405155f1664f216ee2611ee5bf377aa4d4df446f50950fb", size = 6210930, upload-time = "2026-09-12T10:32:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c6/4d20c03ab55d93c5db018511b386da3ec2c006318ccdb03ac398bb95eb91/pygit2-1.20.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2759b548ee9c5812cc34660c02076aa4a92d9a0c75678fcbf7ca9def9120bd7a", size = 4898207, upload-time = "2026-09-12T10:32:38.446Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2d/9fd4d078f7f7f05c943a959792342edf0f061b47239a6bffc7ee79c5ccb9/pygit2-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4455105391f0ca6e35f5d340348ad98811de5a09f50fe832b6f44f8d97286f08", size = 5946490, upload-time = "2026-09-12T10:32:40.319Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cd/1a0fbdf6c9067f5f1a0f88bac3407e436cd4c195f4094e25e09cc89afacd/pygit2-1.20.1-cp314-cp314-win32.whl", hash = "sha256:bec861767a185d281cbf71620ecfe92cb529cd8a9acf3fa18d0820accae9debc", size = 1045316, upload-time = "2026-09-12T10:32:41.851Z" }, + { url = "https://files.pythonhosted.org/packages/56/74/cab8d7a5d6c2a2a33a6fe26a55ef15c7e9181340496dc64e7e823c672087/pygit2-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:b6630a7a61dbd831b2731ac715257851325daa839a3d1251d27f968e33866a19", size = 1369480, upload-time = "2026-09-12T10:32:43.138Z" }, + { url = "https://files.pythonhosted.org/packages/d2/98/bca715d8fc4b0446c9d1660d7986ced967306c94631f082c4ffaee5e3d3e/pygit2-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:e7b6704ba134bf6d91d161844771f8501b909adf8feb8a479d8f95477ea253ea", size = 1076267, upload-time = "2026-09-12T10:32:44.391Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/d70bfa8e10b46eba6c37ba53fc5dcb1d9a3e396bffd4969bb25a79c3f0f3/pygit2-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:befbfc4841e8018de7ffb364675449dbea847b95ddf4d5116da07ed9566551ba", size = 3258740, upload-time = "2026-09-12T10:32:45.935Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a5/2b68dea362f47659bc6d8d6814be799305e6451236459a0b6954b9aa1944/pygit2-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ecb9382e94a7cc55339c7dd0024c011746a400543f61e518736012effad4fb4", size = 2744554, upload-time = "2026-09-12T10:32:47.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/08/d8c3ed6dbd0cb95f078a4c10d357b5e6874850dd364134bed85b99d19169/pygit2-1.20.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10f872e4b57f7172ae07fb7f0080f4681ccecf9e779a816a0c6e55a0f96921f9", size = 6145426, upload-time = "2026-09-12T10:32:49.369Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/c1777a6ac78589a5a29896b777ccccacf2c40d35edd6cdff9fad3865545f/pygit2-1.20.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8c8ba963914a9797548a44baa798614a93f222f8cd41ea2ca3cc1e91a911f88", size = 4741608, upload-time = "2026-09-12T10:32:51.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/19/71d2d0abe632a85efe31defe1493279dce7b6c8509168d9b686a49bebbce/pygit2-1.20.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ebf99b3eae022e8d67141cd89f73ab408f93870a0a3a38f4372c5c7b107346e", size = 5905229, upload-time = "2026-09-12T10:32:52.787Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4f/6a58698dfc5896137f7fa23be5abc4cad11f702222a1f79deab6abedb560/pygit2-1.20.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7261f02e88b1dde453f340534eca6d70116a952e2ee6949b0f061fbe75c01dc", size = 5389744, upload-time = "2026-09-12T10:32:54.612Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/95b6282c3cf69b000610f9a148a02b11e70c40da7bc6ee72837ac5b46c1e/pygit2-1.20.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea9e46030542223016880664a12b6387be6da6f8177f90b4f96b6f26e2e59b23", size = 6273119, upload-time = "2026-09-12T10:32:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/4f/da/aa486ae1884c414b8534821b1e1f076fd1b8c8a96648830ad1c54ee5d86b/pygit2-1.20.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b0daf388b21f71c3e5e52a1168911feef36e6a5eff32a0c1bf78e23ace1e2d1d", size = 4949384, upload-time = "2026-09-12T10:32:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/ad/88/0f5b738f7a6af41eb167ee712ce38697497701a6f8466e63d1195e397b68/pygit2-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cb313dd02e71d2b79b512040ebc5ff15189043d00c550594e2df920ab51bea1", size = 6002373, upload-time = "2026-09-12T10:33:00.08Z" }, + { url = "https://files.pythonhosted.org/packages/b8/aa/a0b3ff4afc0e576b18ca727bea12f240599d0af50bc88c180ce61521883d/pygit2-1.20.1-cp314-cp314t-win32.whl", hash = "sha256:0217a3432b7af85c2946126b9369a16d5b4b4e7a61207b825a3d680d757c8561", size = 1048716, upload-time = "2026-09-12T10:33:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/e67e42409a9712eeee5d183739f8b56847afb44c6941c13f712ced18255d/pygit2-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:030b2d60b82ff29ab66b73ec76a6e15298019d0ea963f8882ea6b7cc1c48fe0e", size = 1372904, upload-time = "2026-09-12T10:33:02.932Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/ec33d8cd06e4b3a5699f6bebb42900aa9e8c2865d228928bff649e64ddab/pygit2-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:57473456976183d2b74e4ad4804e515ed648ed5fafe2c901ef166bcbd386668f", size = 1077634, upload-time = "2026-09-12T10:33:04.19Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/fe735e2b1abb609f49cc61c5c0acf7d1d8a5908d010c8bb14d7b1d012cc2/pygit2-1.20.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bb1ec65b486c1f06bb51945d73cb785f91ee34cdcee85217ed277aa68c429fe5", size = 3193991, upload-time = "2026-09-12T10:33:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0b/75c68b8b401a9b54b7a20905de6ad8e498a7334e1c7df54c18819bea7c44/pygit2-1.20.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b15a1c21f39a8e1fc3d09179d9ad435f8db9ad1a39ed54afe8c00f8b9b6fab2f", size = 2688969, upload-time = "2026-09-12T10:33:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/ca/39/840330696d4db6ea297a61d1f2b4ecc3d386af6a6c17c9e400b8a6e435ef/pygit2-1.20.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0faa5da9a2ca5bd54b02157497588c46f1d2ca463868718922923f79b05191fa", size = 5579901, upload-time = "2026-09-12T10:33:08.704Z" }, + { url = "https://files.pythonhosted.org/packages/78/bc/172cc1d3396057936b02682fc83e4da81c896cdae19732fbbb23f604d1df/pygit2-1.20.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4a1af34170d1f8a1d788c8611a7465a4a834baa7daa3d380b1ef4391726e23a", size = 5335974, upload-time = "2026-09-12T10:33:10.147Z" }, + { url = "https://files.pythonhosted.org/packages/4c/91/4df8fd36d9b6f6eeb8477e643d3633670ef1ea5151af258a72baab2ad0c8/pygit2-1.20.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6477884c76a5197138bc2abd1a5e36b6d45a8cf836290fa06289adb48a641437", size = 1302283, upload-time = "2026-09-12T10:33:11.535Z" }, ] [[package]] @@ -4926,14 +5022,14 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a5/5197bfd06417837ac079921c66fa6393f1dea3557272a263cebfef69e432/pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4", size = 120513, upload-time = "2026-09-23T16:56:00.689Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/40e45bf052ee8ee12a4dfd785519660f8effa7b065442b91646ec6828619/pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8", size = 33680, upload-time = "2026-09-23T16:55:59.241Z" }, ] [package.optional-dependencies] @@ -4956,11 +5052,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.3.2" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" }, ] [[package]] @@ -5006,16 +5102,16 @@ wheels = [ [[package]] name = "pytest-env" -version = "1.7.0" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "python-dotenv" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/49/08ee056f9cc655e437abcf2ae399884844b623223476ae6a77244131db03/pytest_env-1.7.0.tar.gz", hash = "sha256:0c1dc1101fb8d3ab3611e8f8d657ba06c3c0c167fc85c90457e5b27f2508f43e", size = 16408, upload-time = "2026-07-21T13:09:21.834Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/72/d3e125d18f798b2968430b77dba991c69df354ec649db4c1480ac9bc2ab4/pytest_env-1.7.1.tar.gz", hash = "sha256:f2c5aed2621dbfc73c2866a710e2e456409495c5ac10905612c9509b3ee631a2", size = 17091, upload-time = "2026-09-08T14:49:44.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/fc/9f2975c41d41bf5bd9a7d0fc03085ec20052b456b079df53828ae4a1b100/pytest_env-1.7.0-py3-none-any.whl", hash = "sha256:9ee0f1fe859d23fcdb533fe2909a404b3b133d02674a56df275bbe4df4eb104b", size = 10263, upload-time = "2026-07-21T13:09:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/e9/82/fd9f2855bb9d48022942c2123f287d006b7569ae9b2cae8939f94a25fdf6/pytest_env-1.7.1-py3-none-any.whl", hash = "sha256:22341b945305b65ef4e53d7c678982cfd64b5c5800afdf7d37caad2362ea002c", size = 10361, upload-time = "2026-09-08T14:49:43.288Z" }, ] [[package]] @@ -5156,11 +5252,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.3.post1" +version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ed/fa23d28713004418bbf2407127f80f385bfb0bf4e677bef02c25c27b00ee/pytz-2026.4.tar.gz", hash = "sha256:464303645bafafd72418898368b2429458f709cf1eb6a15372fbcc396b64da63", size = 318444, upload-time = "2026-09-24T03:58:40.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/e3df59614bebe0ca50c905442603eda64ae7ac19811d4e4f74aa2d51ca85/pytz-2026.4-py2.py3-none-any.whl", hash = "sha256:9d514388fbc89ca0833203464272ac485b8828568ab73532f5020f17e892a0ff", size = 506747, upload-time = "2026-09-24T03:58:38.629Z" }, ] [[package]] @@ -5263,16 +5359,16 @@ wheels = [ [[package]] name = "readme-renderer" -version = "45.0" +version = "46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239, upload-time = "2026-08-28T15:18:31.132Z" }, ] [[package]] @@ -5319,139 +5415,139 @@ wheels = [ [[package]] name = "regex" -version = "2026.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/9b/ce31551f7191af763ea8414f58289d0ec18ed1d9a531fdebf12dfb249db4/regex-2026.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d416ed4058ea38f69884f3db523b0414c0094d262dde63694aa254e32a433e6", size = 493961, upload-time = "2026-09-01T00:49:33.851Z" }, - { url = "https://files.pythonhosted.org/packages/ef/62/fec41d97d45c0ff82b609012d69813500d9682d88577b70332ea36b131af/regex-2026.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:107319f437fc382e1e445d0abf8923db07f76e5fb3245ac5b09604e5dc8d4f63", size = 295226, upload-time = "2026-09-01T00:49:35.809Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/a35c61819ddd788bca7dc7a1be48a6bf5456044e3bb681553a15611879a3/regex-2026.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05e9f7d16b42686fb38b1702071a7359469ba89e9d516e2ba5228e077dcac524", size = 290643, upload-time = "2026-09-01T00:49:37.169Z" }, - { url = "https://files.pythonhosted.org/packages/fb/6e/f105bdb0ec755f6a1e5f563f6afbcc22b34427c4ce11ab7f265d2df216cb/regex-2026.9.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39d8d5490ba4b8f26ef2aef72b775b1e7fc1a5ebaf28d11e637eb27b0b6a3048", size = 785257, upload-time = "2026-09-01T00:49:38.69Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f0/96e18e5b2a63f7aaf9ede0a1da656e5ebe795fbec035169a54aa021b89af/regex-2026.9.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4381151a29a7b9307842ff444609c4b9a402775fbe9afb3ec3e34ec0396bbae", size = 854330, upload-time = "2026-09-01T00:49:40.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/5e/d6515ad82fb43e3d740abf33db479dd47ce7928c14feb37281ed762571e6/regex-2026.9.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0efb99024ad5ba9198ffa816156b3319c3164b5a8d1e35940d92fdc9158b8d9f", size = 899547, upload-time = "2026-09-01T00:49:41.77Z" }, - { url = "https://files.pythonhosted.org/packages/99/d8/b4a74d131e33f800bd774b85ec2d269c9fb9bbc3f82a06c698bb7da6a5c9/regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ab7ad8b557d1f21e8b4a97a2c1a2ac82cf10c92d6a875c90f038ca9ed85dc5b", size = 793625, upload-time = "2026-09-01T00:49:44.126Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f3/9d21f325b5ff3a95cd79486e37d810f08b72ae26a2be5a795cbe7be3f70e/regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2a36e654181ecb996241bae256d28f26b03d019d6ef65c61dcf44396ab07c548", size = 786296, upload-time = "2026-09-01T00:49:45.78Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e3/bd865f20ce6ee293dbb4c102fe930f2a46ec09daa25ec0662e5af2c41060/regex-2026.9.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d36e9097a1bc6eebe8858216ccd2326f561662de4e50c27533452491c1cf1685", size = 768520, upload-time = "2026-09-01T00:49:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/41/bf/28c8ab87ac526784173d84a7b83fdaf8192037051660415b96f732bf2515/regex-2026.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c407afee6ea4caa313e815814e76d8447fcd32f81e3331d7937c2d33ab80c1b", size = 774325, upload-time = "2026-09-01T00:49:48.43Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f3/8c3704dc3a477d3974e4ddac1f8722a9da3847bf45474821ac3e06289b1f/regex-2026.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:82c27460ff2ea683159a204db2f9ce0f9549dec7070aa0809fbc36226bac1816", size = 848352, upload-time = "2026-09-01T00:49:49.872Z" }, - { url = "https://files.pythonhosted.org/packages/39/c0/83457e7847eeda7d53bb4e8f6e2ceddb96002303d4b670f1cf87310cc029/regex-2026.9.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5e49096c90364317897b8d9fe97e2f86063dc0fc0bcd352017ea6fdd703ff25d", size = 758565, upload-time = "2026-09-01T00:49:51.241Z" }, - { url = "https://files.pythonhosted.org/packages/68/48/4b23395c8d9e9cd004e0785f270259c65a3185118a527ab4b97df842261f/regex-2026.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:11f9b104fa7b23f736b50fe1980a1422eb4641865e82614a25413deabccae575", size = 837119, upload-time = "2026-09-01T00:49:52.821Z" }, - { url = "https://files.pythonhosted.org/packages/e7/58/0912f7fc2220e69a05ea8f4bfd647d1c4017bf37c6d45f30e107367bdc30/regex-2026.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:92e06a5ecabd6e8352da58b9201ae241a1e7382c1419defa9d2f64ad4ea6abf2", size = 782371, upload-time = "2026-09-01T00:49:54.687Z" }, - { url = "https://files.pythonhosted.org/packages/e6/56/194a09928d12c86956996ce87408885588f58a88ada042594b3cb196f5f7/regex-2026.9.3-cp310-cp310-win32.whl", hash = "sha256:6ff5a98456194621f757d6226f88035c00ebbeab0a9095dc9b5a9ec5cc94b50b", size = 266768, upload-time = "2026-09-01T00:49:56.205Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/e59cb72afa57b509346d12e36bf62ecc4322ca5588494cd1b2aee1876247/regex-2026.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:14f8969a92ed847f78e8eed81c6a0384a9e8058a56eb659f83ddb59879c2e4e3", size = 277994, upload-time = "2026-09-01T00:49:57.424Z" }, - { url = "https://files.pythonhosted.org/packages/85/3a/2d1253c32f2f5059398b943329b920748efa062c25b7bcafa9b9334c5931/regex-2026.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:32a32d2664e602b20e4b9c11234cb32f2e985323e453b2b396ea4ae686d82052", size = 276961, upload-time = "2026-09-01T00:49:58.907Z" }, - { url = "https://files.pythonhosted.org/packages/db/fc/b4ff5d9550796f9f955b6c2470f37e2b582ec944638f3004499929a69c96/regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65", size = 493967, upload-time = "2026-09-01T00:50:00.23Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/ab60a24883b810a4b29065540f56e6f72590bb97d704d7e0ee7bcd2ba98c/regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4", size = 295227, upload-time = "2026-09-01T00:50:01.804Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e6/e3bf61522cd82eda85a90dd8f69c839f0ce1da64dc6e87f803bb79267c59/regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e", size = 290639, upload-time = "2026-09-01T00:50:03.244Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b2/e8492b054c5c9fe09bc0aefdc308b8f845ec2094b1ae779accec8f14dedb/regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328", size = 791712, upload-time = "2026-09-01T00:50:05.539Z" }, - { url = "https://files.pythonhosted.org/packages/48/03/429ee3479bb438f7b2b41147fa68d63a7ef9c7486fcd3960e04e61133cf3/regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71", size = 861711, upload-time = "2026-09-01T00:50:06.988Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9e/6ebf3cec538ed49d0a4cf5729129fe39580ddfa80c1c59c4274651762202/regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e", size = 905894, upload-time = "2026-09-01T00:50:08.437Z" }, - { url = "https://files.pythonhosted.org/packages/19/e4/a7bb1e4038a170714c824dc7c739f8b911cb1172f87ab950be39e0691f95/regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01", size = 801135, upload-time = "2026-09-01T00:50:10.308Z" }, - { url = "https://files.pythonhosted.org/packages/bd/47/1b318fb249d5fa3dcb0375ad284d34155f0da4a2fe92c8f01edef0cb0258/regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141", size = 773578, upload-time = "2026-09-01T00:50:12.125Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/63f2644a0a3e92623f83ccadd04873916f5b650e2028bc44ee9df104a839/regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc", size = 783915, upload-time = "2026-09-01T00:50:14.081Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/f078963d1dbd3632f6e25f7d972cc9a9332649ed239995bfeb403cf88a3b/regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b", size = 854924, upload-time = "2026-09-01T00:50:15.826Z" }, - { url = "https://files.pythonhosted.org/packages/53/46/457ac94a2a2aaa2ab5c076324dfd7e38e76f36d4d78923c4ddb8c3d93d75/regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124", size = 763172, upload-time = "2026-09-01T00:50:17.682Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/db145a6ad935cdc6c694381543d53e6e7d7212287d4f8885f965e91ea4a4/regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f", size = 844983, upload-time = "2026-09-01T00:50:19.572Z" }, - { url = "https://files.pythonhosted.org/packages/76/00/100346c5cc2384a7ce0adfb7dbb11f4a865c76e3a920fe68542042da66ea/regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce", size = 789532, upload-time = "2026-09-01T00:50:21.142Z" }, - { url = "https://files.pythonhosted.org/packages/39/62/d878c1d3eed2768f19e8b6fa4aa81b0a0e6bc23fa34e64173ef48816844a/regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420", size = 266766, upload-time = "2026-09-01T00:50:22.863Z" }, - { url = "https://files.pythonhosted.org/packages/e5/da/6a0ac27bacb39aca3452cdd7d1bfc361608ad000cea3e1b0d7dbaa1e2b15/regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc", size = 278007, upload-time = "2026-09-01T00:50:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a2/b23997814f48f823bcda615764b4fb00d72a5a06bdbb41cafa895ec62428/regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8", size = 276960, upload-time = "2026-09-01T00:50:26.101Z" }, - { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, - { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, - { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, - { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, - { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, - { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, - { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, - { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, - { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, - { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, - { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, - { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a0/322f050f77289a1acf99b5c400fe201780bdcfa8f8876fbe250ae3de37af/regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886", size = 496701, upload-time = "2026-09-01T00:51:24.146Z" }, - { url = "https://files.pythonhosted.org/packages/91/30/85df182aec58aaa34b02a3f0a99068bf34a7c9b3f9bd7ff06d58aa866e1d/regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6", size = 297055, upload-time = "2026-09-01T00:51:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/27/8f/64b8bbd0316baaede047dbdbf2bcc96f10a4eeddec2e9cbf289e855e636f/regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7", size = 292008, upload-time = "2026-09-01T00:51:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/b8/87/224614ef16bc8336269a84b578fb0dbab1433b584aaefc5f0680cacf9227/regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944", size = 796341, upload-time = "2026-09-01T00:51:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e1/10ad988d3b673db86353666c56cb32e1b6246ca1ab0c81018a58ebec889c/regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b", size = 866311, upload-time = "2026-09-01T00:51:31.592Z" }, - { url = "https://files.pythonhosted.org/packages/08/16/8d597f97bb6215876512cd7b3c47179c0eb79d1820ab2ed8412d3ba5599a/regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8", size = 911869, upload-time = "2026-09-01T00:51:33.926Z" }, - { url = "https://files.pythonhosted.org/packages/89/e3/f6bcb26472873b9308df329f507d9c6cb526e3f9aed847498f153f2b7539/regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec", size = 801375, upload-time = "2026-09-01T00:51:35.779Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a5/38c7cd736bfd58b190412dbf5a81aae1c38456c538d5c4987f5980cbfa3d/regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69", size = 776522, upload-time = "2026-09-01T00:51:37.81Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ac/8ecefa1c9b57b29fcbe963797435e69a3ba6a9773d802b3d4fca4c75ecf6/regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7", size = 785727, upload-time = "2026-09-01T00:51:39.995Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4c/af402c5aa95615ab66620d775de07ea09b0fea206cd79e3c8a5716d5fc96/regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c", size = 861375, upload-time = "2026-09-01T00:51:41.829Z" }, - { url = "https://files.pythonhosted.org/packages/dd/7b/b83dd26470fa4cc6d7beee9b3c52ff0e988ba636c1367a04d4b2829d8d95/regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc", size = 766221, upload-time = "2026-09-01T00:51:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/94/5b/3b91b19d8fb11a353738cec56b19440bf181f3f9a0298d91140fb4061778/regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69", size = 851737, upload-time = "2026-09-01T00:51:46.708Z" }, - { url = "https://files.pythonhosted.org/packages/2e/be/ea0762fd8a12fa711f1a626283f6d79a3d8a81df4eaada4fbe82009e46f6/regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380", size = 789455, upload-time = "2026-09-01T00:51:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/d1/dd/b9af9d65de78831fb76cb8ef420db4094295b09f93f8abd17c867261c4fb/regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd", size = 272528, upload-time = "2026-09-01T00:51:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fe/ecb15616ae7aa4892299b9ca7c20ef0dd6e5c833643b7ed46e27ff5fcccd/regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846", size = 280812, upload-time = "2026-09-01T00:51:53.211Z" }, - { url = "https://files.pythonhosted.org/packages/18/4f/60efc748152c82b967bd186c356dd1fc21c4f6c225b8026b52547ae60c9f/regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16", size = 281095, upload-time = "2026-09-01T00:51:55.052Z" }, - { url = "https://files.pythonhosted.org/packages/d2/cc/aa269583e8986be9604ef5fb9e2164f33108f6220c896efff2dfb0d4e4c4/regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16", size = 501014, upload-time = "2026-09-01T00:51:56.902Z" }, - { url = "https://files.pythonhosted.org/packages/22/51/bb8a3f3a47beccbf92c3bcda67c2c0336c13c2e10f9744306275f428ebc9/regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d", size = 299376, upload-time = "2026-09-01T00:51:59.121Z" }, - { url = "https://files.pythonhosted.org/packages/48/ec/c646df2b62c1b1334e28581715ed776814c8ac80c0c9106175f0c61ce12a/regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595", size = 294424, upload-time = "2026-09-01T00:52:01.113Z" }, - { url = "https://files.pythonhosted.org/packages/d2/18/3dd8c49cc4af440a9a1a0ebb2b4a939398398c81fb7fecbc3c7d534924a1/regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17", size = 811421, upload-time = "2026-09-01T00:52:03.289Z" }, - { url = "https://files.pythonhosted.org/packages/71/32/c35f031aec040b9136189e3f403681e3168f1f9862e31d9df92ea599f2c1/regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970", size = 870110, upload-time = "2026-09-01T00:52:05.746Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7b/931d962f9fbca3326f282a01fa5bed4d6734dab5215d1c20bac0ab85df58/regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3", size = 917489, upload-time = "2026-09-01T00:52:07.703Z" }, - { url = "https://files.pythonhosted.org/packages/46/1a/da0c28f2b1e65d46a24abc1dd33e0dc9ddbcfeeee7c5625103b3e09e548c/regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5", size = 817470, upload-time = "2026-09-01T00:52:09.686Z" }, - { url = "https://files.pythonhosted.org/packages/d4/a1/6a540ec40c3b8eeefec445362cf08bbb686b22d4088f97d3c1ea38323434/regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181", size = 784742, upload-time = "2026-09-01T00:52:11.778Z" }, - { url = "https://files.pythonhosted.org/packages/fc/88/75500ca27ba85256eb46119ebd99ec10ead4c3ffacd47fe7dfe034662960/regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef", size = 800609, upload-time = "2026-09-01T00:52:13.817Z" }, - { url = "https://files.pythonhosted.org/packages/67/33/18541aaad8b88977e2c3fbbb53c1a75f6f6c999800a82d2f7306581cb6b6/regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae", size = 864798, upload-time = "2026-09-01T00:52:16.137Z" }, - { url = "https://files.pythonhosted.org/packages/16/62/ffbfbeb5a3f2cca701afb9f8aebbfcbf37a4aae2f1584c96680142aab445/regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88", size = 772892, upload-time = "2026-09-01T00:52:18.567Z" }, - { url = "https://files.pythonhosted.org/packages/23/14/04e2db28f490b1fcc684428bd2de21e1ec46f7d8d421b7f2cc74d120affd/regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177", size = 858065, upload-time = "2026-09-01T00:52:20.674Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/c0cac930d1dc0b86c5d8ff10602f15df4fb15fa3f33e04a98be197f33618/regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e", size = 804739, upload-time = "2026-09-01T00:52:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/01/12/69e6f4d02e11a5cb45033abede6e04b0da3716b1b6c311d4609be14a01a7/regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f", size = 274514, upload-time = "2026-09-01T00:52:25.224Z" }, - { url = "https://files.pythonhosted.org/packages/28/64/064b78028b52c0c21b9c0b362b1bdcee7179f009891ddf959d9b6390b30c/regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16", size = 283797, upload-time = "2026-09-01T00:52:27.358Z" }, - { url = "https://files.pythonhosted.org/packages/f2/46/fbb41eb6cc92403ba9f896dfd56a1d5d31549c4033dfb7418da7ecd5802a/regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5", size = 283444, upload-time = "2026-09-01T00:52:29.632Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1a/825beb1c803d579d1119723235ede2211949a8d320bcc79e3f6e5dd6090f/regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9", size = 496929, upload-time = "2026-09-01T00:52:31.593Z" }, - { url = "https://files.pythonhosted.org/packages/ff/54/9b8255521680d54e1d157ed6532e3082998f47925d2ab3861550e0e89455/regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361", size = 297050, upload-time = "2026-09-01T00:52:33.459Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8b/0e4df28fd94304050efc2373dff1803e5d62056cf744a694eaa376df2fe8/regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab", size = 292296, upload-time = "2026-09-01T00:52:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/689a8403f309a7a3f3f3e74b3e81b144602481d122c9e4070a601367f8e0/regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5", size = 798376, upload-time = "2026-09-01T00:52:37.235Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a9/3d22278a38bb8adb254d345e60935316222091caf00a4233aaef388d636e/regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be", size = 866607, upload-time = "2026-09-01T00:52:39.461Z" }, - { url = "https://files.pythonhosted.org/packages/69/ef/ecf8c5fdcec73ac0526fbd074be3bd1059b11cbda739ac0ab9001f311aed/regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e", size = 912160, upload-time = "2026-09-01T00:52:41.603Z" }, - { url = "https://files.pythonhosted.org/packages/68/e8/c07c3d2b44c9154672a33911cfcfba626c78d939d7e31103864d81ad319a/regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb", size = 804589, upload-time = "2026-09-01T00:52:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/91/16/d42957c78b5934efd03ca5713d99952cbe71e2d0b3b24d9e78256f208587/regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f", size = 784446, upload-time = "2026-09-01T00:52:45.866Z" }, - { url = "https://files.pythonhosted.org/packages/00/1e/4252dff33a1a5afb3271fb3859e992d34c47c040ecfc27bbfe6a7eaf99a8/regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c", size = 787996, upload-time = "2026-09-01T00:52:48.345Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/90d31048bb402fd746a95922d7dfdb9de9f902c39c8d838f2fef05c9c92c/regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27", size = 861752, upload-time = "2026-09-01T00:52:50.597Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/42ad133550cbd64d8fb33ec5ff55e1f524b06c6328e9f3bf9c3e8652b525/regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7", size = 773488, upload-time = "2026-09-01T00:52:53.142Z" }, - { url = "https://files.pythonhosted.org/packages/78/8f/0c90e8fa607c36a65c360f6b02980aa7aa5885afa3d737daba4229bef086/regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253", size = 852603, upload-time = "2026-09-01T00:52:55.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bc/e1be309afb6be94916d8f4f000dbc66040fc0da2008b3f714dc4893259fd/regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20", size = 794635, upload-time = "2026-09-01T00:52:57.611Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f7/15f6d0ea137316531c992e7468a4c6f6e39159a6780c2c16ec3ab56f2149/regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae", size = 272530, upload-time = "2026-09-01T00:52:59.644Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/739168818790278dd4f1ba60c582fed41323508d7a8e850323851de1aa1f/regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57", size = 280822, upload-time = "2026-09-01T00:53:02.57Z" }, - { url = "https://files.pythonhosted.org/packages/12/6a/55f5f6e92a8c7d6511e069d3e8a4b45e3506941c1a97dddf5a329b132be4/regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950", size = 281102, upload-time = "2026-09-01T00:53:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6a/adaf9ed10ffdef60684bf8757c257400500342503f1ce5c7cffcdb2e8aaf/regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4", size = 501333, upload-time = "2026-09-01T00:53:06.681Z" }, - { url = "https://files.pythonhosted.org/packages/64/dd/2cbc3b253a88fdb0c8ca641367dc2f280541fdf1722f201fc0be62797577/regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff", size = 299273, upload-time = "2026-09-01T00:53:09.002Z" }, - { url = "https://files.pythonhosted.org/packages/2a/29/4818d31ec2555309b3935d3cd006a7f6076e11bb21eec9e72afee3db87b1/regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76", size = 294888, upload-time = "2026-09-01T00:53:11.009Z" }, - { url = "https://files.pythonhosted.org/packages/94/37/8970a4102a067e0a28e40a232bf536752c373da8a4f932755679f77fa9ac/regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96", size = 813072, upload-time = "2026-09-01T00:53:13.566Z" }, - { url = "https://files.pythonhosted.org/packages/90/1c/2e52f5e9e92b79e324c9807e977fa6f9ab210b41ab5b7356307d5ffb2f4c/regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35", size = 869947, upload-time = "2026-09-01T00:53:15.773Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/c676e3f2dcb47ec941fa678019044cbac51db9441e09cec47392e4986013/regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa", size = 915678, upload-time = "2026-09-01T00:53:18.384Z" }, - { url = "https://files.pythonhosted.org/packages/01/6a/73c971bcaf1d24e73aa3c8e0f767caccfdc9fcda021e59f28f6dcd12728b/regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23", size = 817714, upload-time = "2026-09-01T00:53:20.723Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/0e9bffb6cfa8a00e765eaea0d1e6d207bb98e620d5658c537c8d87df671c/regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec", size = 792411, upload-time = "2026-09-01T00:53:22.925Z" }, - { url = "https://files.pythonhosted.org/packages/61/d4/b1d226e8a3d0c8aac577a517384ffaea7b9ff98b194ddbd0bef58b9d8738/regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de", size = 802521, upload-time = "2026-09-01T00:53:25.281Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ad/dc47f94a4ace898561dfafeed6052dd3551915f36aa1971f4387d6daf0d4/regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f", size = 865266, upload-time = "2026-09-01T00:53:27.667Z" }, - { url = "https://files.pythonhosted.org/packages/41/7b/743565fc25cb36981186b4e9b42b3d093a7226cca328380cfd223c1b5a03/regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544", size = 780298, upload-time = "2026-09-01T00:53:30.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ad/d62e48324632eb0bae2c336da5810b51a14a9e23ba964f2828cc1ac23011/regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092", size = 858045, upload-time = "2026-09-01T00:53:32.397Z" }, - { url = "https://files.pythonhosted.org/packages/2e/41/8eff42a2516494bb6b36d33b15bcd861764dcc836ad7c0b746e46c513af9/regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69", size = 805406, upload-time = "2026-09-01T00:53:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1d/98198fd6dc185a172f5de48b8a4c7ca03c0d4285b1c1c32300672e7cf85a/regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a", size = 274674, upload-time = "2026-09-01T00:53:36.77Z" }, - { url = "https://files.pythonhosted.org/packages/d2/82/dc0e76842ccddc8756471adff38c140d130c6b601b9ae5724274f1881e7a/regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803", size = 283692, upload-time = "2026-09-01T00:53:38.951Z" }, - { url = "https://files.pythonhosted.org/packages/da/3d/e0677f590f3473a3defb63ef6e3469154382a2a15ebafb364482d7ed3b01/regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2", size = 283421, upload-time = "2026-09-01T00:53:41.393Z" }, +version = "2026.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/f403115361de25809e8f785686ec7096e30fef73be9ae35aa51da4e80abb/regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d", size = 417072, upload-time = "2026-09-09T21:00:21.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/29/b4160140513c37ea8e7d87c467b5b7e658f53ca4cdcfe394b82ec768d0b4/regex-2026.9.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7dcad477c49c4c626a6c4fcd71b39a971aa217060cc40a6569fd24edcc0fa509", size = 493887, upload-time = "2026-09-09T20:56:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/6008e74d6076a980a48cb3eeaf80c1324db73f2eed89a8023e91669dad84/regex-2026.9.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d414c411c06fe0009eac33488fb1591c66b5c2673e342e452e7bb2fe63da8194", size = 295102, upload-time = "2026-09-09T20:56:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cd/2f540fc813d1f71f18e1cc6861483133a0e6afb349a4e770fbdce4811d8d/regex-2026.9.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f2f43bf4e47ff7ce9e585558706d698c6204d0f80bf2207766382ed817c8e9f4", size = 290660, upload-time = "2026-09-09T20:56:23.835Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/9c990517587418d82406203d4608e17e02167c73fb36ef9a7a9d7ae64802/regex-2026.9.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d12a625a37c89c2b09303402a06942f55f071b95a7916a49c17034c3d47cd5", size = 787864, upload-time = "2026-09-09T20:56:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1f/bcc010de82e20c1822d22ae94d97faafd46ce8405599f3636622a9a7bd04/regex-2026.9.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef4ce69ff97fbb44b46751cfea5e859ad0b66d1a50abf34954f0645f51e81671", size = 860487, upload-time = "2026-09-09T20:56:26.363Z" }, + { url = "https://files.pythonhosted.org/packages/58/3e/6e75baf9c78d90bf44187f5ce4ee5de51fde206e6981490a0a4418cde296/regex-2026.9.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:044bd4639b6bb409ec9e5d8b7accd57e02b4c4a4e2eafde916f8ae8006b3e40b", size = 905374, upload-time = "2026-09-09T20:56:27.741Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/be9fafe5e05888296fdd35d82ba9cbb67b0d352e9911fad3c458beb3a34a/regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d95d7d9538b5b726dd6fcd7b6117a71e6565202f6d64f5845fb4d8f203f533", size = 796252, upload-time = "2026-09-09T20:56:29.074Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/4df639a88ce4fcdb0aedaba7818718254b888e4b60c7df640d5fc82232cb/regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa911fbbcf8ac90bda0e0657d60768e3390954ef0588211d63a22add1cb1cd1", size = 790214, upload-time = "2026-09-09T20:56:30.586Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2f/2bb3fef68f7ba134d45a2aa3c713078109ebca71ec5dbb84a42d7284abec/regex-2026.9.10-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ecb2e7acb18f8cc4a67f0ad986c0af291ea4dd385d0614ba9bc09d7f8bbb478c", size = 773413, upload-time = "2026-09-09T20:56:32.223Z" }, + { url = "https://files.pythonhosted.org/packages/7d/57/43737678e306ad6430732828ecf0632c72676eab783cc98c4da373e7ee96/regex-2026.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4971776b4f2bd7fd9a83eceb2cb2592cbe2924f639fe8045e6a9de5ba4bfcf25", size = 778331, upload-time = "2026-09-09T20:56:33.51Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/61f4158219d60ec727500f08f6f0aa86a036bb5c3ba0632204f35eeb92e8/regex-2026.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f8bdec659a8fa7af51a32b224b3b7c02bc415d54ffd35187b1d224176b17d607", size = 853498, upload-time = "2026-09-09T20:56:34.931Z" }, + { url = "https://files.pythonhosted.org/packages/6f/85/7c783b98eb777cd17e2566ea6343a350f6a7c13f41f43282c20348ed9303/regex-2026.9.10-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3540734dbe241ebb3b87d5713781f6749a3e4d45480f506aa5fb5cbb0c37d249", size = 762708, upload-time = "2026-09-09T20:56:36.248Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cd/a56fb347afd102dca21fc3ee9163ccf379678154ec2d0e3f139fb26e5eb9/regex-2026.9.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87f5f75c109f08f5c602d68e1af54cead8165189c727b6ac946b30b9833a3ba4", size = 842488, upload-time = "2026-09-09T20:56:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/59/54/5d38dc952ec93cd19943d31647806c7fbf380c74f36b747874b26a3070fe/regex-2026.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88b02aa8d0ec9b6189fe933d425775882271c23700ac11fd26d1779b0f56fde3", size = 783557, upload-time = "2026-09-09T20:56:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/63/81/c4c880460f22160046e9d4794d947935da8337d579e08c3492344be32d1d/regex-2026.9.10-cp310-cp310-win32.whl", hash = "sha256:13c52fc377792675f604a207a2ae5958c080f6854f7698d40d9ff034d95b1e76", size = 266938, upload-time = "2026-09-09T20:56:41.352Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/5061dde95447f00acdeeb39780b32def0ce380c3e725aeee1f5b37b788bb/regex-2026.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:e6b99181d184d0f5c7b36b8d12b94d1e9499cce6246594331f9edc5d2ea9fceb", size = 278251, upload-time = "2026-09-09T20:56:42.54Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c8/b3f8dab9592d1089fb2cec58f5c1120ce1c5c74b2d12d50119e404187ee8/regex-2026.9.10-cp310-cp310-win_arm64.whl", hash = "sha256:abbfc1c33bf8efddcc43844aba61e036d74a918680dc3ce8ce2538b004eda0f9", size = 277308, upload-time = "2026-09-09T20:56:44.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/82/36fdcd669c7c47e11bea67e3495011ec365be0942aae3ba74f82f92ed6a2/regex-2026.9.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:debc629e98b95abaea1cf3057ca296151f348c697c9b8a59d18013adb302c0dd", size = 493891, upload-time = "2026-09-09T20:56:45.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/bb6a8273bcb53077a1855fea0ff416fe4d4bcb1d9f350a9fe3b909853983/regex-2026.9.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d772586951d7d6a5d162d48f414065e483b1c81ab38fd8ed97c78b05883421a", size = 295107, upload-time = "2026-09-09T20:56:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/38/71/9d96d04c36556f057398a08dfc21aecc5409b5fef83fd7653a310227df7e/regex-2026.9.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b43456de605c8ee77eb75f07bc1ee44ba27f9cee22207deb77d495e954b7d953", size = 290649, upload-time = "2026-09-09T20:56:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/af/cb/4dd3b7190b5a169c81a4d63b487d65e9fb23689b771f5aceb91d9e7f65bb/regex-2026.9.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0a8b4928823bc8b217a1ab7bf3d90598909dec9a70fbbfe9a52cc4eca55990", size = 796358, upload-time = "2026-09-09T20:56:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bb/aceb2329a2d54a3bbd95e54f08689692414674c213e8fc958cf253206e5e/regex-2026.9.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b91c37551bf39d75116c02b146956f65b9aa0337a4a652f4ae186983789d4001", size = 866258, upload-time = "2026-09-09T20:56:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/ba/fc/f8deac9c4c475b6bc35f25565a2253df4d27d49dcb6b189d9432ae69294e/regex-2026.9.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b9ba3b2765cdfe18f0f561a69f78a69701f2896654a81c711108d35d14e5099", size = 912779, upload-time = "2026-09-09T20:56:52.811Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/2dd4d56932f55ea412c9808f27a8661f93568bfee596ed04bc57a74e591f/regex-2026.9.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0acee94b480dd853e39434aa9a575f95385b1b4b8fa3feae56db363ca5cad782", size = 802962, upload-time = "2026-09-09T20:56:54.241Z" }, + { url = "https://files.pythonhosted.org/packages/46/a8/43edbb94e6c9caffd08bffca5b5c486d18845569fcad3953565b85dd71f1/regex-2026.9.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a41693eb3fc4b92e6127d113813c6c395237f7edd3224abf67609af48c690d11", size = 777136, upload-time = "2026-09-09T20:56:55.749Z" }, + { url = "https://files.pythonhosted.org/packages/78/2a/6de302fe3dfa94b75a414bffe5180c146967f96fc52fd0b3527a05ad7c40/regex-2026.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6888065672b341e5246f391ec16dc258a29218ac784172fd67c30d941544755b", size = 786593, upload-time = "2026-09-09T20:56:57.211Z" }, + { url = "https://files.pythonhosted.org/packages/ce/22/66bad9464b4b9122180d2fec86209d7f1bcaf4388d34f644139799f94a20/regex-2026.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8c07021a4faa3f092869adbd1f35cdc7a592276c807aeebc3ceb8ff1a638f0b4", size = 861247, upload-time = "2026-09-09T20:56:59.194Z" }, + { url = "https://files.pythonhosted.org/packages/3a/90/a862d6fd9ee3bc0c1f3a2fecffb325097608a4c9b57f0e35030f68c878f7/regex-2026.9.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:58c01f7b81079cf0817ba831ff4d9eff5d28be4a3ac76c353e6f09bd63f4c386", size = 767925, upload-time = "2026-09-09T20:57:01.031Z" }, + { url = "https://files.pythonhosted.org/packages/98/4a/aea909c0699aec3c5ee6eff93e4dabc0d310cfb0a5618094e1c5228b234d/regex-2026.9.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f0e2e5d23448b660d60a6ed85c46cc03b4b48bd276b8f4041d4a5fe2a4a0626b", size = 850772, upload-time = "2026-09-09T20:57:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a9/a55f1257d95871b0e042b42bf52f931b3a92b4d530def5201c5b9d597e20/regex-2026.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94d096369b7cd96d15343fef5257fe39eff9d0e8758b92a0e15e358b92cdb2fc", size = 791475, upload-time = "2026-09-09T20:57:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/60/96/04bb3dabe9ac7aa64ff758dc34ae0378d60f5279ccb66788e1f837a0cac0/regex-2026.9.10-cp311-cp311-win32.whl", hash = "sha256:7f8f10015866608fe4c043cec2e4fe4c39a94bb50e45091de4cdf4004b9ae4b0", size = 266937, upload-time = "2026-09-09T20:57:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7f/2d39cd798871b683409cd14ab4458f60e53db86591028a8244c3155f2e92/regex-2026.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ce7c118cb102975f974585688357a717ffbf9dddd64ab0bb1bc93eb5b367cf95", size = 278263, upload-time = "2026-09-09T20:57:07.089Z" }, + { url = "https://files.pythonhosted.org/packages/13/5f/eb3a6fd0ef50719d105e77bcc8a94b5b6f4d2873819fcf481279c454ce73/regex-2026.9.10-cp311-cp311-win_arm64.whl", hash = "sha256:1e954e246466d5a1a78f563ce8364b5d7cb19e7adb0ccdec8f9c9610083187bc", size = 277305, upload-time = "2026-09-09T20:57:08.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/c8/bfbe893e90ee0148bd2860dd086f09b5d2080ca2b125f740c2e118c16982/regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db", size = 496609, upload-time = "2026-09-09T20:57:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/56d5ae6efb759255c3b3db650a4be25f96a844ea3613b91e1e189a3b7294/regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a", size = 297024, upload-time = "2026-09-09T20:57:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/0f2d5f6bbb791cc10f22f0ed16c487e630dde8fa8fa0bd92a2bfe21a4b20/regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb", size = 291905, upload-time = "2026-09-09T20:57:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/3fb5fe0d32f4cf0bc982286722c729a8d6f522d2fa2d5d14a702d9fc87f8/regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1", size = 800055, upload-time = "2026-09-09T20:57:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/ee507bd2f9d4420f26a594b35c551d7194b66f5d7897f63730fae6ec05c1/regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4", size = 871133, upload-time = "2026-09-09T20:57:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/cbaa90689684f91b1bc017e7f8c6d9425c6bd299108db02482dc51376d8a/regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd", size = 919627, upload-time = "2026-09-09T20:57:18.402Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/dcdf5e0d898024005cfcce631e3e934d111dfbe177ca0b7f253ae8a735a2/regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0", size = 804587, upload-time = "2026-09-09T20:57:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/eedfe81c29bae266a06ab4250978361a9bccd474704d88d4f4ef4506dff8/regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3", size = 777320, upload-time = "2026-09-09T20:57:21.62Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e5/86b207077efbcd91305700488f170b7eb1e1c54721cea74175273aa3b9a4/regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8", size = 790572, upload-time = "2026-09-09T20:57:23.048Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/f622c3b2323f4c035b98e80221740a442127ad7993135b814f52057430db/regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23", size = 865485, upload-time = "2026-09-09T20:57:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/34bd621d3d6ac906ad67e57ed56c40cd45f7d51b9c0328335e97a7cb8ecb/regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944", size = 767925, upload-time = "2026-09-09T20:57:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/0d/28/ddbf7cba86f2adf5038c6c16aa829636ffc6e437f81bb0cbf302899cea5e/regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0", size = 858800, upload-time = "2026-09-09T20:57:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/e8d7656ff3d3bd32e881d32f540b4981c79dee61908d6b790a45966e6895/regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1", size = 791648, upload-time = "2026-09-09T20:57:29.786Z" }, + { url = "https://files.pythonhosted.org/packages/fc/65/eac1a79115c8475d8ff539602eb54031564d719fdea5a599c4b0a1a26d1b/regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348", size = 267326, upload-time = "2026-09-09T20:57:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/4dcd0a05d3e97ca829165df1c39717f899d1d484dac8a6032439f2cb8d6d/regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86", size = 277933, upload-time = "2026-09-09T20:57:33.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/22617a80dee28f2451011eae36bc26b3d78c4994ba87b5281d60acf9b6c0/regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae", size = 277447, upload-time = "2026-09-09T20:57:35.156Z" }, + { url = "https://files.pythonhosted.org/packages/20/90/d4452bf1ef7dbe406980e8b921a257024482203c1dafac535eae207611bc/regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca", size = 496408, upload-time = "2026-09-09T20:57:36.757Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/c763c6424a0f99d021d46dc1f9065147bb5a40c2b2cdf28d2ebdbcd96508/regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da", size = 296931, upload-time = "2026-09-09T20:57:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/fa/68/241f88458b17c46ed2f80147a60a03b2ada7fb815c23b6bc76c298abb0a5/regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd", size = 291741, upload-time = "2026-09-09T20:57:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/974d6de404c63e2d09525f4ddb99874c7ab8e1f781ccbe0dd3e26fa6f6e5/regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383", size = 800088, upload-time = "2026-09-09T20:57:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/3875b73f9e7ba3321dcaa02c19f650c05c61345328acf84599ac6f45ceed/regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1", size = 871212, upload-time = "2026-09-09T20:57:44.03Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/2358e791c0e171194dd6a8b97b520579098a21397fb79dbe6b7edc9e3fa7/regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4", size = 919752, upload-time = "2026-09-09T20:57:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/000c79c3f9c06b7542225a5d3a7f9a85405da7224b3b9af94a491d07abea/regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041", size = 804578, upload-time = "2026-09-09T20:57:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/195eedb1de87f26639191e7487e41eb81e2ce255bc7563a64f3f5a95eb08/regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b", size = 777345, upload-time = "2026-09-09T20:57:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/11fe2b313fcd92cb75c583648f2746031b9f4da9e9ed4241204a5e8b3721/regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0", size = 790556, upload-time = "2026-09-09T20:57:51.27Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c0/07ec9b4c43b0e16d62454971a5ab3886eccb0bfa161300a02d801ab28620/regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406", size = 865572, upload-time = "2026-09-09T20:57:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/19/07/43bc9a9cf9fc8e37d2ba47980dfe4a6e151d2cf3ab969e0031e2a9b21484/regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502", size = 767971, upload-time = "2026-09-09T20:57:54.805Z" }, + { url = "https://files.pythonhosted.org/packages/9c/49/3b9286a3a94f3c89ed4ddbe74e72bdde21c1a5eadd520d5f4ed4a61936cb/regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7", size = 858835, upload-time = "2026-09-09T20:57:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/e5d27ce9fee8e3ef95f886c7b6ecec211efa4cfc18bd73bd5cf26cca4741/regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643", size = 791793, upload-time = "2026-09-09T20:57:58.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/03/c28a6bebedc3e2d86ee27ec2de16f7ec0419dcd10e771d43dcc9c58a2e99/regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5", size = 267298, upload-time = "2026-09-09T20:58:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/5c85fa6cfb8e034080bda5a72fa0a4df2b7777a35eb7e73c2799c2adda7a/regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4", size = 277894, upload-time = "2026-09-09T20:58:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/c1/28/f5a25f6f65501675977fda35d9f61abb1468c4b87c0f73e536d8b21a60b8/regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7", size = 277436, upload-time = "2026-09-09T20:58:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ed/98e9b07d8bb9c765d07774f0b2c19b301b96d51f44630fea48951051c94e/regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5", size = 496662, upload-time = "2026-09-09T20:58:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/9dfe9be48378b47c5a8f04b0f200ea225f9ee0f8e93f010433f661a37878/regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468", size = 297115, upload-time = "2026-09-09T20:58:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/5d1f005a3825ec49842ad349061c1c26e6d42f47ccf105e6e5aa6aeed392/regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f", size = 291896, upload-time = "2026-09-09T20:58:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/be/15/44ce83fca50c6058f42b62fa8300a8030eea7e4e5a973a2dd33db0f557fb/regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a", size = 800534, upload-time = "2026-09-09T20:58:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/a62e070a5a033643b287489576f02ae6a9c584c337d62349e340a2b4d001/regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0", size = 872038, upload-time = "2026-09-09T20:58:12.186Z" }, + { url = "https://files.pythonhosted.org/packages/f3/06/8b8e2483949b1329df10c5b615e85d332066deb426b11332b799629b9201/regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876", size = 918927, upload-time = "2026-09-09T20:58:13.903Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/9b56f69100d3afdbc9c4fa6e302764f9cb717fbc06a9d50558d98ca89cd2/regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864", size = 803694, upload-time = "2026-09-09T20:58:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/d00d59a7c8fd0e070ae8457a8743597f45ad9682b100f57b9c9c405fbdfd/regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742", size = 777770, upload-time = "2026-09-09T20:58:17.628Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/a11d0446484efbc9eb67abec133f254c6d66a1568b8f3fb36d39a73a1129/regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd", size = 791234, upload-time = "2026-09-09T20:58:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/bcd24e78b373fd4f98090caa43eade439223f6703b909be79b9efd9ab0ab/regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89", size = 866259, upload-time = "2026-09-09T20:58:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/c57ba5e94222a813260af4c17ced92d40cdb44737eb6b50979688310b6a3/regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c", size = 768219, upload-time = "2026-09-09T20:58:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/3820037587d00901ace96c5864335c2cd1b899d5263ea0dd2261359e0bee/regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb", size = 858582, upload-time = "2026-09-09T20:58:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/f9a838ca6219ae4821de0175e4548db73ef56de5ec08d032fb427732fa07/regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76", size = 791405, upload-time = "2026-09-09T20:58:27.406Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bf/67d71cc4e13ae2e0022d21243ca069e0868701a99eb29cdecd13d356e694/regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405", size = 272702, upload-time = "2026-09-09T20:58:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/c1/38/40a93e72703a741235115ed1b1e5f6b869917677b7643005034ce1611d70/regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77", size = 281170, upload-time = "2026-09-09T20:58:30.899Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ac/e95387f00617c16bb41b786d900bacd17eab88afa81b4f263df532b3a731/regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d", size = 281511, upload-time = "2026-09-09T20:58:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/e5/a4b12262edc488a8a7a95b672db317dd8aa9bf2fab98297f9c91bb11ad4d/regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666", size = 501128, upload-time = "2026-09-09T20:58:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/55/c8/9ca31c0fa5197ded8614c8ae0e105bcff2d979025ffa3c75580baa334e2f/regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd", size = 299428, upload-time = "2026-09-09T20:58:36.296Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d1/ee7662561735f90475443c3ca1977e5cecaeaa8f29620dec75580aebc839/regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26", size = 294494, upload-time = "2026-09-09T20:58:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b1/333168e45ed6cfe71f6d17e21e5f54725f44d171f84edd12469a2f739227/regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db", size = 814925, upload-time = "2026-09-09T20:58:40.438Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/df1b38536d0a3b24a030eb4130ce98b403cc925220ff530f5313a7c436eb/regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7", size = 873323, upload-time = "2026-09-09T20:58:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/93/ea/aa71fe62dd63a8336bbdec1ff002a6c53b40ccfca95961c18ee4f09bf03c/regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843", size = 923080, upload-time = "2026-09-09T20:58:44.488Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/49f8d6fd34fc1a9c75b5b96364ebdde702a8144e5ed63d2213c58d454c27/regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe", size = 821284, upload-time = "2026-09-09T20:58:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/91a977c96d4be13d1ade8208c260c26841c836d4c91745e1828a30589070/regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7", size = 789256, upload-time = "2026-09-09T20:58:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/4d110bf01bf9651bf9b3f88a6d9fa7e643e0586921a18432b25a478edbfa/regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c", size = 803722, upload-time = "2026-09-09T20:58:50.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/b587a0d3bbac2635309ed9c40c197120c39e1ec0afb8813cbed1338fdd75/regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a", size = 870085, upload-time = "2026-09-09T20:58:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/0827bca20ddba1d70fa5111a2a64e6b6b38bfdab43fc435022b54172a111/regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f", size = 776970, upload-time = "2026-09-09T20:58:54.478Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/ab9d08d30568ca868791bfb99551db60b947f05e2e65dcd1261e87083c21/regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7", size = 863611, upload-time = "2026-09-09T20:58:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/8063ae86b543ae878a7d6e7ba21ebf0af4af06161230e0012e2d652320c6/regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca", size = 804064, upload-time = "2026-09-09T20:58:59.066Z" }, + { url = "https://files.pythonhosted.org/packages/c5/63/b19305c4b8d3d7867699f7d83c43550273d91a2f31793452d87edb1d259f/regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873", size = 274607, upload-time = "2026-09-09T20:59:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/1695072a512a49060294024e23b945eeb87675b02c06e53a92a1b42b0bfd/regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf", size = 283944, upload-time = "2026-09-09T20:59:02.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9f/65bac17f39991a67e22f8b3c849fdc02a56147c97029b5351494341959b4/regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07", size = 283780, upload-time = "2026-09-09T20:59:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/1d1f83bc2f8fff4f186266ac82d73254e686565530cec9ab5228fb5c63dc/regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077", size = 496869, upload-time = "2026-09-09T20:59:07.051Z" }, + { url = "https://files.pythonhosted.org/packages/33/42/4217510286501a2ebcd372b781b4754ac961e043fa13ef8dce803c44d89c/regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2", size = 297121, upload-time = "2026-09-09T20:59:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/595468ed0bbccd94be92c6b5d67736ba128b204d942429a8485c2693914d/regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c", size = 292139, upload-time = "2026-09-09T20:59:10.859Z" }, + { url = "https://files.pythonhosted.org/packages/29/1c/ac92c123e0ab9bea75a904272171b356940bd6139e4a35de44f2254dca8f/regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924", size = 802375, upload-time = "2026-09-09T20:59:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/8d/16/d349f6fa9f908162359004e4f067353a0146e8074d24989490778244be44/regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91", size = 872328, upload-time = "2026-09-09T20:59:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/df/81/251b5aef23147057e926346fdb7c8c352d0568f65a492e4e9eb6120f6446/regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e", size = 919594, upload-time = "2026-09-09T20:59:17.31Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6e/1f25319dc1b9cf4b7ffa303f3d16ca53260fe92aff45e81aa1b3c6c7cba2/regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f", size = 807394, upload-time = "2026-09-09T20:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/3da6b3dffddf12d5e96cbbe6f5e65ebcd64f92e3388a6690a53e0878232d/regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231", size = 786018, upload-time = "2026-09-09T20:59:21.592Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6f/1cc86dafddc912ef44c5ccd4f729060be8c6735600932664eb6d0e25469b/regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840", size = 793504, upload-time = "2026-09-09T20:59:23.734Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cb/11692e29388d006211163627fcefa0c79917ab9f94d46a1b2254fe5efc81/regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b", size = 866766, upload-time = "2026-09-09T20:59:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/3631df969830f94d83fcfc5fc71a7b39caad905e18f7b17350a94135d625/regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325", size = 775825, upload-time = "2026-09-09T20:59:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/762892a8e3e21d119eacaffde848aa247e6cf4e1d90c46011671e86b1b9e/regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487", size = 858901, upload-time = "2026-09-09T20:59:30.656Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/78048fada5c2f61d795efb26ea24f820a5564c4aa26574920284d45cd656/regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376", size = 796208, upload-time = "2026-09-09T20:59:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/58/632681f7b9aaa3d83b40e5862ba46364a453c8eb2bc7d43fece3dc31f972/regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb", size = 272704, upload-time = "2026-09-09T20:59:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/5d/64/81cce28754c37037b1fe740b6d7a556d51d97cf935ea4547bfab104f43f7/regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d", size = 281182, upload-time = "2026-09-09T20:59:36.86Z" }, + { url = "https://files.pythonhosted.org/packages/33/28/5a13a340c9c759e863a0e7f765d323601d03d6538c8a627ed628662e083b/regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4", size = 281512, upload-time = "2026-09-09T20:59:38.875Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/a3caebcd5105be90708071db20bd261b0961b8ea4fe5e8be45c2632519b5/regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1", size = 501336, upload-time = "2026-09-09T20:59:40.987Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ec8887b2658bf0a5df143c7c1fcd562b0abd2fa208c04ebf15c6607c9bb2/regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f", size = 299318, upload-time = "2026-09-09T20:59:43.039Z" }, + { url = "https://files.pythonhosted.org/packages/d5/58/84724a9eccf6e8cd46f7e4534576093e2476a5eeb55e2453aa6606e86059/regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37", size = 294847, upload-time = "2026-09-09T20:59:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/70ccc5e53905984abf8eab63eebd3ce740522ef8c8d392622b64d79ef290/regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727", size = 814359, upload-time = "2026-09-09T20:59:47.194Z" }, + { url = "https://files.pythonhosted.org/packages/29/53/40f7a11ec547e4a947883c9d5e8a075f6d4f59af2b8dbcaa8bf5b504aca0/regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b", size = 875586, upload-time = "2026-09-09T20:59:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/83f3e022d99ce601727c4ef5f7b527753901ce4509ed89a1bb6a2263380a/regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3", size = 920990, upload-time = "2026-09-09T20:59:51.776Z" }, + { url = "https://files.pythonhosted.org/packages/b2/64/dfdb367d8f4f5c9b8ccb4b59789c2ce996f2c69c3b8192aa986fe7d92ec4/regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8", size = 818660, upload-time = "2026-09-09T20:59:54.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/fb7d0b64487913845834f319aa84f8377d55305958a5db7e19c128798366/regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c", size = 794976, upload-time = "2026-09-09T20:59:56.799Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/23484edaae387ea0d31f4d414463211e3516c8aa210ce8d0640f5aff3502/regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc", size = 804081, upload-time = "2026-09-09T20:59:59.39Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/e30d138f13aecec99ea9aecef7e31563de6ec6a2f5ce1d65fc506aee33fe/regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b", size = 870430, upload-time = "2026-09-09T21:00:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/81f9ce86f7ae4f57901543597c95751fa01c41a672ec1636dd913f8a000a/regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1", size = 783327, upload-time = "2026-09-09T21:00:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/d7bf8f91534740f6a8ca17e5ac9c5337903baf527c8de240fbac1bbedfd0/regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548", size = 860874, upload-time = "2026-09-09T21:00:08.41Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/52cc88364aca7c9013dfe9abe0fea67f8d394efe384d07798300a6e2f27d/regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d", size = 806705, upload-time = "2026-09-09T21:00:10.768Z" }, + { url = "https://files.pythonhosted.org/packages/cb/51/37a194df7707f92f33173260ba7b221d4ec376419a53babaec50019a2804/regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f", size = 274780, upload-time = "2026-09-09T21:00:14.003Z" }, + { url = "https://files.pythonhosted.org/packages/77/12/3227a52970d90908b230f15b2c86df903f49ac72c9eedf4f6e8b5bb5e1a7/regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72", size = 283936, upload-time = "2026-09-09T21:00:16.275Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/c567c5a61671f04d30e83f20b496b465432879196574d051007285576205/regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c", size = 283747, upload-time = "2026-09-09T21:00:19.113Z" }, ] [[package]] @@ -5505,16 +5601,16 @@ wheels = [ [[package]] name = "responses" -version = "0.26.2" +version = "0.26.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/1a/4af3e6d659394b809838490b144e4ab8d7ed3b9fecc7ca78f5d2f79b1a3d/responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8", size = 84030, upload-time = "2026-07-03T16:44:50.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/47/f216a33221db8eff328987661cf18371afee89c62a62b434b963d6b509c9/responses-0.26.3.tar.gz", hash = "sha256:b0c11ca8131b8b227b8d5108e6ed39772222bd5aab030ed430e8f99057c4c409", size = 86335, upload-time = "2026-08-26T19:17:24.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/28/693e1d9ebf72baa062ded80d837a035b86ce75eda5a269379e9e2b1008a8/responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364", size = 35609, upload-time = "2026-07-03T16:44:49.1Z" }, + { url = "https://files.pythonhosted.org/packages/6d/86/ca7958de70cb0752350575e98229368a3a2f746a2942034b3364e17312bb/responses-0.26.3-py3-none-any.whl", hash = "sha256:74474f799334ac4f37d93b6437ecc3bb1bb5c77a8d31780a338643be2dce0af8", size = 36289, upload-time = "2026-08-26T19:17:23.176Z" }, ] [[package]] @@ -5816,27 +5912,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, - { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, - { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, - { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, - { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, - { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, - { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, - { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, +version = "0.16.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, + { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, + { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, + { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, + { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, ] [[package]] @@ -5916,11 +6012,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.9.2" +version = "2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" }, ] [[package]] @@ -5956,7 +6052,8 @@ name = "sphinx-markdown-tables" version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown" }, + { name = "markdown", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "markdown", version = "3.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/36/af0e0a4aa69298b4d664d5176243412ae96bfc207d7e3314e12b3a6f2ccc/sphinx-markdown-tables-0.0.17.tar.gz", hash = "sha256:6bc6d3d400eaccfeebd288446bc08dd83083367c58b85d40fe6c12d77ef592f1", size = 15189, upload-time = "2022-07-29T00:35:46.641Z" } wheels = [ @@ -6081,14 +6178,14 @@ wheels = [ [[package]] name = "tencentcloud-sdk-python" -version = "3.1.169" +version = "3.1.181" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/25/0aa04c111e6a76f1737d269835ea67312e9c63fe399d73df698d54a4c9f4/tencentcloud_sdk_python-3.1.169.tar.gz", hash = "sha256:56ae413abf007c1b5a02059b8fbffe8249b815bc481fc2f8bce4b1633c104a5b", size = 16931418, upload-time = "2026-09-03T18:56:38.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/93/a2bdc0840f12dbc372112ffed73172aa1146b10117694059f58fd07f0deb/tencentcloud_sdk_python-3.1.181.tar.gz", hash = "sha256:d3bc02d2fa5626dcaafd2c4653a70464e0b0772346ce517d089dfbf5522a5c1e", size = 17378611, upload-time = "2026-09-24T20:01:52.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/3b/90da2b98c73bccc27afb10699d50a07afacf6d7859b54a8d8add5d9cdfc8/tencentcloud_sdk_python-3.1.169-py2.py3-none-any.whl", hash = "sha256:58edafa0ad8f8883c91db4e13d35146ecb71a806d92017d46f5c91c573da554c", size = 17854559, upload-time = "2026-09-03T18:56:18.745Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/e664e40c6f3471e369cb95aaf72d0de764d847e596a8c9437366d40ad978/tencentcloud_sdk_python-3.1.181-py2.py3-none-any.whl", hash = "sha256:a048b66bd7963955abe9a12b4443ab0f67dc325cf6c4a07a8b9116c40056af25", size = 18316048, upload-time = "2026-09-24T20:01:46.267Z" }, ] [[package]] @@ -6102,37 +6199,47 @@ wheels = [ [[package]] name = "tfparse" -version = "0.6.21" +version = "0.6.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/ba/6f2c8a46f64b2738095d20c9347d05bfcdc5cf02f2599db3e1d8a9e544ec/tfparse-0.6.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:63c6ee072c312bfc28e68489087d178f2b4ff5f3b78aac3f1f743fbd9453501d", size = 16285404, upload-time = "2026-08-22T02:06:41.644Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b4/d0c37b43c53df7f8e452c4f4c9224ee090b654adc8fbd08e6df99ae8bfd4/tfparse-0.6.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e83ad92eb1ace8bc6d61c8113cb6aadc64fb4427dd3e753bb2403d32d21aa0ee", size = 14964259, upload-time = "2026-08-22T02:06:44.065Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/97419a79c52abfbb8f755e39e719223e2d724ac7c03eaefaa6909285667f/tfparse-0.6.21-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb97d5ccee6ce916e1f80e66c1601ff33d5aacad8920317c3044e347a8e7677a", size = 17405906, upload-time = "2026-08-22T02:06:47.254Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1e/9dae1c36eda82425b811e99fa92db161baf349b5a714410f16b75d59b5c2/tfparse-0.6.21-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d54b4234fddff00f4e7054d6f5df2f31bf99b5f295749788944db07dec35289", size = 15698116, upload-time = "2026-08-22T02:06:49.466Z" }, - { url = "https://files.pythonhosted.org/packages/63/1f/01ef184524996a826042d9c1822cfeafc3ef618323f3fb425459725682b4/tfparse-0.6.21-cp310-cp310-win_amd64.whl", hash = "sha256:79a871bf33df0cba6bb23c7f89cf6cb9eb2396bf82bee3a90df614ba8a8bf1b6", size = 16590355, upload-time = "2026-08-22T02:06:52.286Z" }, - { url = "https://files.pythonhosted.org/packages/03/2c/b3850fa7105f27e9912c301e2a8343e01e827f575da27451903bd732c383/tfparse-0.6.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d81c8425312f1380a07c9318989b88516f27855c871035217e0865d0aa5616c", size = 16285398, upload-time = "2026-08-22T02:06:54.454Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4e/cf06015071a4da464de6f69ac44b5374bfaeedac788996a6804416389b36/tfparse-0.6.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fbd07371a314d81a1751c5502a6c94d3c0fa6ecdd368b3a1568f3253e6bb1d3", size = 14964302, upload-time = "2026-08-22T02:06:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/e3/30/65d0aea3dedaebd351a39bc764ac1b427253492c5330046750cf99761340/tfparse-0.6.21-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:003f3d84921cf40a622aa6ab11587fb8545aeafa0f0bd4dab5d6a528c38d14f7", size = 17405906, upload-time = "2026-08-22T02:06:59.433Z" }, - { url = "https://files.pythonhosted.org/packages/97/85/3cca0c0597229985f1843368ffd8abf19e389c1e8f01efdc8fd3d7c39ccc/tfparse-0.6.21-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:baa54e47be8c9c770f22304d9c68b7a61aa351f0bd86022d8b806109d2f26ff0", size = 15698109, upload-time = "2026-08-22T02:07:01.83Z" }, - { url = "https://files.pythonhosted.org/packages/40/8e/b58c313c2995207dcb2009b15524e3b3e35269252ae02230da7b1602e4a5/tfparse-0.6.21-cp311-cp311-win_amd64.whl", hash = "sha256:c3e5850eed26324ea4dc1f08ed75b2021f7b17a3e119d705e9a83269e2177177", size = 16590813, upload-time = "2026-08-22T02:07:04.731Z" }, - { url = "https://files.pythonhosted.org/packages/51/ce/4639a6697b8c428917ac87febf872903f4fece84412b537fb66fd379955b/tfparse-0.6.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dda7864cd25cdb227d30e778b18afe645ab8c7891eb0d4939c7c7e647d7076ba", size = 16285392, upload-time = "2026-08-22T02:07:07.058Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/fe84ab62454a5a6dd80998af735b68746c4eda9d26e2097644af4caec2d5/tfparse-0.6.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b22910a0c6d3380c37e41e7dad6a03c13fddfe5f0e50020e9f90171c7dfd0775", size = 14964275, upload-time = "2026-08-22T02:07:09.477Z" }, - { url = "https://files.pythonhosted.org/packages/18/c3/aefc1c8a513a24d3f325f0970bf32002b8f192f81673f36f347d5ef84193/tfparse-0.6.21-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c8d47eae599bc006393853f9965feeda344d37ab71a12c5d841ef474a2aabdf", size = 17405906, upload-time = "2026-08-22T02:07:11.844Z" }, - { url = "https://files.pythonhosted.org/packages/28/e3/288eeaff6697676088cb2be7c1ec393ea19b48dd502f06a88eb08a148117/tfparse-0.6.21-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b489332282c399db74111558854420a8550e010699665d2d6b31980edb1f83", size = 15698114, upload-time = "2026-08-22T02:07:14.296Z" }, - { url = "https://files.pythonhosted.org/packages/2b/88/4c738b5bc25704b72228fe05d19da0d71460f77cb9c1813d4d0d86edc057/tfparse-0.6.21-cp312-cp312-win_amd64.whl", hash = "sha256:efb6532c44599dae33ac2b81654f1cf083cd0a1d8403e899f96588814040815e", size = 16590858, upload-time = "2026-08-22T02:07:16.722Z" }, - { url = "https://files.pythonhosted.org/packages/93/14/fab42da0329c8732375975d1d72809b1f0628dbdd9ba532408515afd088c/tfparse-0.6.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0d3829e59cba32b55a7bd3e4faa8012b72e1df513c60117db3bb9b087df3ad3", size = 16285400, upload-time = "2026-08-22T02:07:18.967Z" }, - { url = "https://files.pythonhosted.org/packages/61/78/06a2a6dfa5fa93ce5c679efda98540cb3a5db6740d0a9f651dff1c57c4c5/tfparse-0.6.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef01dbafb6e588764e04ff8482cf5ca53d0196525c3433b86d988cb742c6dc2b", size = 14964274, upload-time = "2026-08-22T02:07:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/44/25/8dd2858235027c9d015b297db2209acc2656f04adbad67573e65c9fd45a2/tfparse-0.6.21-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a801a18d227462731bcadb92a41f349c6a59f942dddec9904d34bb09f2da0236", size = 17405905, upload-time = "2026-08-22T02:07:23.599Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1a/1afc5ffd957d14d41abd2b2aa979162855b2d16635fb1f21694c596cb187/tfparse-0.6.21-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6da9bc3511a0c3ee35b958f437254f3a5740781d4e0385401dcbd7a3d1dde150", size = 15698111, upload-time = "2026-08-22T02:07:25.867Z" }, - { url = "https://files.pythonhosted.org/packages/b6/64/ec50b131ad5043dbdf645c6e7c1e0935198397a05cd20efebdc7a2923d43/tfparse-0.6.21-cp313-cp313-win_amd64.whl", hash = "sha256:41d70492e23ced71e912b8639ca998e525550e4767db7c8dbb88adea4ae7ac4e", size = 16590482, upload-time = "2026-08-22T02:07:28.026Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ef/26f6939bd8f1816b67205601da5cd357354a943dd19dd7d08abc8b7764e0/tfparse-0.6.21-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4238d3ef7f07451c426858f22ce9f5e5d65a78d08fddb2961c491c55275d83de", size = 16285023, upload-time = "2026-08-22T02:07:30.249Z" }, - { url = "https://files.pythonhosted.org/packages/8c/64/e8f84a4e16dbf100f270a563246d74d96f439c76d8fbb164449af42e76c3/tfparse-0.6.21-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:408b0af8acf4551bf2759b68a175bbadb226f37458b245cda98ccb28e6ab271e", size = 14964289, upload-time = "2026-08-22T02:07:32.656Z" }, - { url = "https://files.pythonhosted.org/packages/2d/34/9f63a1e047f97b5e776037567bf3ee0a78adf006ea2a798243573c24d544/tfparse-0.6.21-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d4d53d1c67683806044929fd1d05f3a6ef1b9fff7926d196e7e25f47679025a3", size = 17405900, upload-time = "2026-08-22T02:07:35.029Z" }, - { url = "https://files.pythonhosted.org/packages/de/08/cafb4dcf23705494f98c5d7c96abeaa0f7bd6dea0dc540636cb3645bdbac/tfparse-0.6.21-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a17dd593fad08a71f95280bec1d508587bb847ca6104469f748cf0376c14c70", size = 15698161, upload-time = "2026-08-22T02:07:37.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0d/c1f35d2f0f00b30e9d84b59c44d6a6c241c09f4ace4f3330d4f86780ba77/tfparse-0.6.21-cp314-cp314-win_amd64.whl", hash = "sha256:8c2801b78c25231454c48c46c854e772a4013107fe807c8ce133272f5136e592", size = 16985290, upload-time = "2026-08-22T02:07:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/32/20/add66b6f5ee70d84c9deb0d375ec1b2a44c7ebf2d9bc03f9d79d221285db/tfparse-0.6.22-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3bd38fdd26e7f2273a9f5d74eeffd6c89e700091d01fd5dc2e859458f9832d3f", size = 16286213, upload-time = "2026-09-22T00:08:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/07/04/57ad688fab9ce8e45152c6894c0e8fbeed641cc347730dfca9641e5e402c/tfparse-0.6.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bba4609cb27e5dad2e30977d3e38f2d1c48351512ef856cd9f930e7c91a8ea48", size = 14966560, upload-time = "2026-09-22T00:08:29.372Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4e/98990b1c942843d7b8672d153b9647548268f4c1c454faed200c50520b4b/tfparse-0.6.22-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3fd400101fa3d9e29380069acd6f45efbcb9bb49ce6697129140635d8e3651c", size = 17414715, upload-time = "2026-09-22T00:08:32.076Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/93876f85b60b0c079baa3d93b04187ec7dba3aec78132f01cb0c76b5a93b/tfparse-0.6.22-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ab29df6bed4b66aaf1885e410820393e4057bd00037613a1cce876f0bfbb4d1", size = 15699973, upload-time = "2026-09-22T00:08:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dc/8d725f6e0ec0ea04b27e04bfc621807c436106978771c18dd3d2ee766e90/tfparse-0.6.22-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0080dc6c42b86d7cdff742ea4c1f6287b21b0e0ed3f4012ad5a358d6743c3046", size = 15701701, upload-time = "2026-09-22T00:08:37.759Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ca/1162eafdea5e0471c7c3aa8fb80c7a2c61b1c7957172ea9718a73431c29d/tfparse-0.6.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da66f60949f6e45236641a36a204687f94da0e5d40283d0b42dba0aed3188785", size = 17412489, upload-time = "2026-09-22T00:08:41.159Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/754fdcb1cf73602fa7eb6432a5d409ddfc96591b349b37d324d3b450688e/tfparse-0.6.22-cp310-cp310-win_amd64.whl", hash = "sha256:89e3ff12ddc30037709a0f408bba37b7592663fdc4973c8dd3d71f3adbcc035f", size = 16592396, upload-time = "2026-09-22T00:08:44.421Z" }, + { url = "https://files.pythonhosted.org/packages/b6/18/09aa0182c32be2c6f4bd7ba7b3148ec5eb7dd2ea748dc542ea9f9a4fa416/tfparse-0.6.22-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a8dd7c2351ee0d283e73c93e0faba9d425a398a7329b641108c12c83f00e42e", size = 16286205, upload-time = "2026-09-22T00:08:47.87Z" }, + { url = "https://files.pythonhosted.org/packages/97/c0/47e9667cab1cfea5362746343756b82d42acbeabd1d6e6e2f1a8a5797886/tfparse-0.6.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec9deaac20c70fc3f4016f6332946a681a8a3aa09ab62aa1de792f1e510ffb54", size = 14966557, upload-time = "2026-09-22T00:08:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/7c/73/5cb6c82da6cc80caa0607187dc1095be439e4b09eb32434219f45a70f29e/tfparse-0.6.22-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27ff261eab75d8407b11da421e02bf45a91da5a290e4622d69665f4fde17cfeb", size = 17414708, upload-time = "2026-09-22T00:08:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/97/00/fd6b24c8cb0749b2c00afceaeb09edaf6a8df5af42e65c7b453026dddb52/tfparse-0.6.22-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb620a18bd5d1e7ad6ce02e68c181859af55e60358c4cca5649ce8f6879f59d", size = 15699956, upload-time = "2026-09-22T00:08:58.25Z" }, + { url = "https://files.pythonhosted.org/packages/97/b8/26037d1df07e399375d9b8e221d9b552402a5c2f0cfd8ec299e0eb5d5e3a/tfparse-0.6.22-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:278022fe7f4d8f13c9615957ad862cb57f5fd2714239c876e853a2913612cc98", size = 15701697, upload-time = "2026-09-22T00:09:01.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/20e2defc5cb254de99f9934fc9cbb179f4e6b7bd9ad9cbe8c1a6b09b366c/tfparse-0.6.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2bf780e1d16a974b72fd39a00575d2954796a80c370bd7f2b596e21da82f5b2c", size = 17412491, upload-time = "2026-09-22T00:09:05.021Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2b/fedc1be172763fc96dbc51764376d13621e8dc89088d8b11c840abe26ee0/tfparse-0.6.22-cp311-cp311-win_amd64.whl", hash = "sha256:227be89ccf5760075eb085156a1aa54bcc75ecd418f0832f8f1c41b20347f321", size = 16592748, upload-time = "2026-09-22T00:09:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/be/81/c643c78d0f179b7dbe7cf583edb6b3a35056c86811a8e021c6386eecd2a1/tfparse-0.6.22-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d8f1d8c81561c85cd0c7d75ea855983c2a3bec92102df8275696cc91395622c8", size = 16286213, upload-time = "2026-09-22T00:09:12.049Z" }, + { url = "https://files.pythonhosted.org/packages/81/c7/60c7af278fddeb0e81d235681cc3d974dbe92453be5bebff7e0847ac2006/tfparse-0.6.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aea9e13be2172330aa6862231bbe88dc522892a13bbaa018542686741bd3918c", size = 14966556, upload-time = "2026-09-22T00:09:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b038b608011cd5954ea4e00bbc3a4f972e8a00dd634cbe977c039c3de20f/tfparse-0.6.22-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b4d9ba1b95c9f1a043dce7495cd45cd40b66aae9747959b11802a94e5325948a", size = 17414708, upload-time = "2026-09-22T00:09:19.207Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/95b2c120ef2885743de713d2531c6cf28c36ce889f750c536f129b044a8e/tfparse-0.6.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8505c4281fd64a4f4f4a3a786af261b6e389b40e121db1ee0fbf2a4d16e8565", size = 15699965, upload-time = "2026-09-22T00:09:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/42771c74cbd35b2e0008f561b85aca79a011e46f7c3fc8d80863652707c2/tfparse-0.6.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb22754699a2c1edf8bc185d2a73b60fb3ab9cd46eb84ae3028abcb03bc2b0dd", size = 15701705, upload-time = "2026-09-22T00:09:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a6/efc57a957cf2a388f4a84a15cdeaef6c8966f88a287fb7f21ed4e4845acf/tfparse-0.6.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b702ece7b649cd9d339ba8a026086f5d36cb2a5ff98b317485d998d776fbbeff", size = 17412453, upload-time = "2026-09-22T00:09:29.023Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6e/9f3d2b583e7895b4ac6e7239c029b585ebb673a661602b2bbbc921dbe6ed/tfparse-0.6.22-cp312-cp312-win_amd64.whl", hash = "sha256:b4e52c8b7b3b133572796bdd5bd55f8233a93a7434bb43dc1b98b4906dddd85e", size = 16592786, upload-time = "2026-09-22T00:09:32.251Z" }, + { url = "https://files.pythonhosted.org/packages/c9/04/b4d4f0041e8e74ba9f29e81f7a86e1c869ca8867bc44ec635d5ff55b0389/tfparse-0.6.22-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:931766c10313af261e4dbea558a4cbc073db4600942ee039e3cc7827ef4c2e3d", size = 16286209, upload-time = "2026-09-22T00:09:35.583Z" }, + { url = "https://files.pythonhosted.org/packages/80/2d/40f1905075eaca46364aa781a22e8e2f4fed0f084fecfa18f7f07432b66d/tfparse-0.6.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a3902c264836028988d1e888f9f14fbdd76160cfcaf068372722ef1d79776ad", size = 14966514, upload-time = "2026-09-22T00:09:38.549Z" }, + { url = "https://files.pythonhosted.org/packages/3f/54/f85581b1ae36f816144b73efae2bdc2b74013c5ed3116e514f129a5f8361/tfparse-0.6.22-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a9a2f5d2adf643c317af6b8d9cadf4bb778e5ed31bd244fa612c51e8f3cefce", size = 17414701, upload-time = "2026-09-22T00:09:41.626Z" }, + { url = "https://files.pythonhosted.org/packages/09/3a/3b403aba13c1e1bcd150f24e5cfc67709d1f66caa85757ce0add4d195325/tfparse-0.6.22-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91257ca0323b036f98aea2de94dcd2e4edb27d505681bc05ebfec188c38ff4c", size = 15699983, upload-time = "2026-09-22T00:09:44.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/6b/77d2f365c7c34d3bc970e3a684ad8bc1344e2d295b5ba7a746993c52c7f0/tfparse-0.6.22-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:677fe2fd3d2f120fc138408a233a5404fadd78eccafb2933d5cf7878c714e838", size = 15701716, upload-time = "2026-09-22T00:09:47.688Z" }, + { url = "https://files.pythonhosted.org/packages/18/15/75e76da1524172cb89602a3174e7d500ecf2b7c567f362aa6effae077226/tfparse-0.6.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d19c479b812098cb6219aab36c43cf5435e6849484cca6b79b33dd42831b6b0", size = 17412476, upload-time = "2026-09-22T00:09:50.985Z" }, + { url = "https://files.pythonhosted.org/packages/0d/23/8961bc828bf024ebbe7a7590a899a0cf0af85e2b0feb5fe1732cbe2a802d/tfparse-0.6.22-cp313-cp313-win_amd64.whl", hash = "sha256:c61e5b683f828a9f07697d7954e6bfbe9bf758bbc9e3db3bcd7436c09d8117ad", size = 16592590, upload-time = "2026-09-22T00:09:55.137Z" }, + { url = "https://files.pythonhosted.org/packages/91/04/1322ac7742413c6b4b93c9812faf4fae47f4b7dab511bbeacf37abad7e24/tfparse-0.6.22-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d00cd7e3253fed5aa42d96c40217e3f2cae95d1c4919c774a421131b21de5121", size = 16286639, upload-time = "2026-09-22T00:09:58.454Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0b/0a54b51cc7cd3facd0bac0b362ad8955e10acd52eb63e962ba90dcd5f10e/tfparse-0.6.22-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab7712b72c9ce3dcaf9452326eb2e14d105de8317dce9c37505e1a30b97d8b5", size = 14966561, upload-time = "2026-09-22T00:10:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/75/aa/698d180dcbe6928c9bf76043fd2965f24ef071cdaf4a1179055ce639d4af/tfparse-0.6.22-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0012256f202d68eb0a57295ad2a59d4da6592a6f66809dc0864cae4f8ee5727", size = 17414711, upload-time = "2026-09-22T00:10:05.209Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/f051a6b8457ee81dea577c4fe6aff935858c900f881b2f65b44d58b9038f/tfparse-0.6.22-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:718fc456878b9c9ccdac3ebeb1c83fbc95f6b7ec42ade1a849c6438f6846bea7", size = 15699966, upload-time = "2026-09-22T00:10:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/2755e62d90c680519d55690524252a781bd22aedc242c9345cf8c606efc3/tfparse-0.6.22-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:734126289f49be34a8cf48ba60c8006c1c0d41bd9c174f02af8c19c89af43822", size = 15701487, upload-time = "2026-09-22T00:10:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7e/5b8391ea6caa55e268dbdcea37ddf2583ca44a36467aaed6e95d2916602f/tfparse-0.6.22-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:76ecfe96b783fe1c2da98e73ff1a358e9f0a224854b31743ce00ea5057828f7a", size = 17412495, upload-time = "2026-09-22T00:10:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/7c/51/4193bc2ed8a7b7840150bd394de5a47e054c11f041dc91cf490405bdf1a3/tfparse-0.6.22-cp314-cp314-win_amd64.whl", hash = "sha256:6f61d05ebfd0accbee49881c93ed294c0ce81592fe1642bfb29e410e82a2ab28", size = 16985980, upload-time = "2026-09-22T00:10:18.26Z" }, ] [[package]] @@ -6241,11 +6348,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.3" +version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, ] [[package]] @@ -6271,11 +6378,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -6305,11 +6412,11 @@ wheels = [ [[package]] name = "websocket-client" -version = "1.9.0" +version = "1.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110, upload-time = "2026-08-31T14:08:40.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786, upload-time = "2026-08-31T14:08:39.899Z" }, ] [[package]] @@ -6326,88 +6433,99 @@ wheels = [ [[package]] name = "wrapt" -version = "2.3.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/31/5822ce37ca8820c2ed35a498c67c8b37960b9cee2ba437fd32849d0a234c/wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7", size = 81191, upload-time = "2026-07-28T06:04:04.858Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5a/3c6117938be98754578ab83f5a40d7d0ea2cd2c487dc5cd6027ee7228229/wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f", size = 82255, upload-time = "2026-07-28T06:04:07.151Z" }, - { url = "https://files.pythonhosted.org/packages/a5/0f/94ae724c5087eb6054c0d63febd7094947dcf302fe058e2e0488102a872b/wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43", size = 155228, upload-time = "2026-07-28T06:04:08.272Z" }, - { url = "https://files.pythonhosted.org/packages/6c/21/1f780bba935dcf697c0c59de9be3a559bbb8e31a53ca3f25422023738432/wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023", size = 157073, upload-time = "2026-07-28T06:04:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/73/31/6c7799d7b6431fcd7e1b83245fb45258a2d2c3a2187fbaecb83572a72d7a/wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152", size = 151594, upload-time = "2026-07-28T06:04:10.784Z" }, - { url = "https://files.pythonhosted.org/packages/ce/17/42d670dbfafd49076c6eb2b7d67633d7e1c968e39bfb11a135acb6fac67b/wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02", size = 156069, upload-time = "2026-07-28T06:04:12.316Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d6/c66b4ba4eda49257c84d5c2df26118280f09ca7905aee20d0064db778d13/wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276", size = 150930, upload-time = "2026-07-28T06:04:13.482Z" }, - { url = "https://files.pythonhosted.org/packages/c0/f2/1a3b949c0322fb27396eafd1044328c1cb0400e0b32105d75a3cd03096e7/wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199", size = 154525, upload-time = "2026-07-28T06:04:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/147563a3dfa6e830c857b93b530ebd8c0cd9d540e5914aec8f9b12880c02/wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35", size = 77879, upload-time = "2026-07-28T06:04:16.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/eb/921405b4dc55d4f8be4c700ef120539fdd75d5fdb50d83bd257171ee18e0/wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0", size = 80733, upload-time = "2026-07-28T06:04:17.43Z" }, - { url = "https://files.pythonhosted.org/packages/b6/13/75947450c5bb57795fa86384721cd52c5c4deb0879022f309501a8a85d44/wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760", size = 80199, upload-time = "2026-07-28T06:04:18.761Z" }, - { url = "https://files.pythonhosted.org/packages/00/b8/9182e4c618a847be0baccb68e4602b070d0fa22c782cf058f4bc66b32709/wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe", size = 81427, upload-time = "2026-07-28T06:04:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/84/ca/613cefd9c5977366b1587e61c0b428176d382e6d75b454084c5e58503042/wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d", size = 82360, upload-time = "2026-07-28T06:04:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8", size = 161700, upload-time = "2026-07-28T06:04:22.723Z" }, - { url = "https://files.pythonhosted.org/packages/49/2c/bc508fee75eb2919ed69769800b09968e4aab16897f909a23f39c81e323f/wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c", size = 162922, upload-time = "2026-07-28T06:04:24.177Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e5/04f34d38e66d857dfc2fc4088d60e70c0e422467822defa49b2b4a26e17b/wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731", size = 156125, upload-time = "2026-07-28T06:04:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/23/41/c35940ea1c423f129ebe4361db853bc80d4def6326242e1206fa15bf94f4/wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb", size = 162039, upload-time = "2026-07-28T06:04:27.154Z" }, - { url = "https://files.pythonhosted.org/packages/0e/60/9bda34c3d7d182aa703fe35339ae0ed4c4dad5e5c587f93890143e1f87fb/wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6", size = 155110, upload-time = "2026-07-28T06:04:28.497Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ba/60bfd9b1a751f4fcb2d603668fc272d651ccdd339a56acf8c40ad21a0293/wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd", size = 161089, upload-time = "2026-07-28T06:04:29.959Z" }, - { url = "https://files.pythonhosted.org/packages/0f/32/2bd358c6f4f1305c813479d1e9ba746bebdd794f4a20107ab2b3ee0cbd45/wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14", size = 78030, upload-time = "2026-07-28T06:04:31.241Z" }, - { url = "https://files.pythonhosted.org/packages/4a/62/ecc969b13b141fef89b888c9760821cb01a86ac8fc953911592c8e1e1522/wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84", size = 80944, upload-time = "2026-07-28T06:04:32.655Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3d/9278ada8a2b3f24372b630361e84e9a7de7abc3784634860c26d1c37785a/wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98", size = 80074, upload-time = "2026-07-28T06:04:33.811Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, - { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, - { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, - { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, - { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, - { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, - { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, - { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, - { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, - { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, - { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, - { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, - { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, - { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, - { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, - { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, - { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, - { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, - { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, - { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, - { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, - { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, - { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, - { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, - { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, - { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, - { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, - { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, - { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, - { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, - { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/42/a6/6375d56c44d590ef24acf0f8f5bf7ed768ff7a510b959306ec412611e90f/wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16", size = 164597, upload-time = "2026-09-10T23:12:16.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/45/fe4524dea5a15a6f64522745a96618fb2f874c11c4060848abacc4f85232/wrapt-2.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7aaff952dd6930fc87b52d44bf72ac8426e45e90058fe0f2da446889738eff44", size = 97739, upload-time = "2026-09-10T23:09:36.872Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cb/08025a4402bd6f34ac7c07e4d5c75e4f32e595cd7424d697c6fce1663bc3/wrapt-2.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d67b916f2f777a6d31067e842598e059f1a0e29022c86454564fc9efb9a0c19", size = 98237, upload-time = "2026-09-10T23:09:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e9/2c1a482052cfabd6906887096993099afa2db738e7597c30e4f2b9922718/wrapt-2.4.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2945f4bce1518e09eb25458dbd09800f7aa3fee81a5a5cefe66c4e82fe8cb69c", size = 217028, upload-time = "2026-09-10T23:09:40.259Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0d/867f5a1955de8e04edfbdf32891376443629d94c3e31ae2ea2ba101d6d31/wrapt-2.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7ce62ee24eddb76c317ec790c44f4fbe9f34ee0a5dae37b30c65a2b8f3e3604", size = 219323, upload-time = "2026-09-10T23:09:41.701Z" }, + { url = "https://files.pythonhosted.org/packages/42/21/af01e7d55ce47df290e60cc4c0acccd439ccb95f30be63e46ba013e4f212/wrapt-2.4.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cbee8d00840ac89846f6567c75506260a15fbb4dc74de180678fafe09458d47", size = 208024, upload-time = "2026-09-10T23:09:43.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/9ae249d4922408cb28bc31e8dec86e918350261cf61e38fd8bbfff8bfd66/wrapt-2.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b78dd8d058468156003ee8c212dab334b6eea18cd9c5db3b81f6c90a9408afcc", size = 217712, upload-time = "2026-09-10T23:09:44.739Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/34b1e65e490ae886b057ac813e86e16399b7dae91166b5bde5fc9f86c2ee/wrapt-2.4.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4d48f1720569b4f6e2df97783fd270554abc4c6ebd74b2e5f15e5110283aaf78", size = 206245, upload-time = "2026-09-10T23:09:46.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e5/8b5af45e8e7dc102b78a40b077f071466a0f584898fe57ce181bbca2572b/wrapt-2.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:de5d2d7f12557c9e994b4037330efa5700fccb9f2b9df0bd4ccf384eaf73c254", size = 206937, upload-time = "2026-09-10T23:09:47.328Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/06e483462e90a50250f51da3ffa1f1c67e457910078b36aad55805202886/wrapt-2.4.1-cp310-cp310-win32.whl", hash = "sha256:5c0217b8c12952bf4d137a19ff2c61cc9b626f52c09c43f0289769c9f5f808aa", size = 93405, upload-time = "2026-09-10T23:09:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/01/c200bbabb0c46a431c9d965fff78e6bf08c1598fa970bdf105362aeba969/wrapt-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:0aacc1af512e040fb0b3c4593a8bded9aaa04235025f08a54eda9d8c1103d3c6", size = 98454, upload-time = "2026-09-10T23:09:49.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/04/c7eace579b4c397aa3651c550c216b05d3561cff56bcbeb726c8ab3faf4b/wrapt-2.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:6f32a45d0e883918387aabf5384952e5a93ae40be0a6fb5f1eddc8281af4c714", size = 95247, upload-time = "2026-09-10T23:09:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c6/5e08ef1e8ad5ff518354db77b7a303666c7e0652e2a569116b5ccb72b690/wrapt-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b6ea396edcfb305698f44ef0a47b9adaca15d30386ce093ca606844f776d7236", size = 97984, upload-time = "2026-09-10T23:09:52.569Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ec/e77ba77fdce732f39522382a6a9b968e5abf3471f352d51670120534e271/wrapt-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b86bfe741840cec6d8d5858b1eb53a325f564f08d7e6fb5694981961edcec230", size = 98241, upload-time = "2026-09-10T23:09:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/7b/af/c54bd0bf32ef3c5f6d2d50af6bd9d88c040ef53d9f8e113e065337d76788/wrapt-2.4.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ec7aeb92e956810eedc268fe75f8c11f05d6c3d864d0995a53eb6623b3b6a51", size = 226167, upload-time = "2026-09-10T23:09:54.946Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/934e13c7e27680760fa74e25e4b291b213fd607aa42333d883f732de6d73/wrapt-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba66eccadd4b857a845e309c08457bb71fa8300000e508e3a86cd695bf9c7515", size = 228439, upload-time = "2026-09-10T23:09:56.272Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/7593f3fe527260ff3e8eb09ed26ff8f0bf51cda5702a75fb5969b689b4a2/wrapt-2.4.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d5c8f3b4eae814213d4097f134be03aeb23816e6358213454ec5e62e8449fb6", size = 213495, upload-time = "2026-09-10T23:09:57.622Z" }, + { url = "https://files.pythonhosted.org/packages/42/a8/d4b4d6ac44cb2c7d4a882499786fd96758c97feb6b1539ba0ed5c823849a/wrapt-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3bc7ab495e564449f39db2f54c1033d7190c944736d525f3bd06cefaf0134df0", size = 226108, upload-time = "2026-09-10T23:09:59.132Z" }, + { url = "https://files.pythonhosted.org/packages/05/b3/aedc046bbd8ff2333c2ce9d5267db42117221d4192ac13c66e871b528d9f/wrapt-2.4.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2e2e694057bcfd46f43e28f7b50fad8f5fec7113cc6445fdf251219539a08f28", size = 211144, upload-time = "2026-09-10T23:10:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/96/60/c01efb0e8049a9df379b04ebc589eeef995d6cf184b1d8be9f1b84b4d5f7/wrapt-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5577fdc702427698c81c8c9c3b8479cbecb3dfd0f697be8c1d09c89f5838cc", size = 215496, upload-time = "2026-09-10T23:10:02.778Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/26214472915eb447725982e2735c593131c5bae0c3fd582ad61e7b965907/wrapt-2.4.1-cp311-cp311-win32.whl", hash = "sha256:e78c0f6d66a564bab245faf170d55595df6fc237117b99d97fd838678f0906f0", size = 93493, upload-time = "2026-09-10T23:10:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/5f7d2dd40c97c5e572dce771b8d4c1c78d46a90cfc24c7b25f158253215a/wrapt-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:1f1851be0e593d65e68d7c1bc9d3b971e19fefa219f617e490d75e80e467ee14", size = 98679, upload-time = "2026-09-10T23:10:05.413Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6a/f7565da9ab1323b2aa5fb4a83b919b6519cc0f4ea4b91ec8f2d25f5bcbab/wrapt-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:7ff549316d94c404999f96bb0a6993222566231ff4bede1b6f68908d1958a08f", size = 95071, upload-time = "2026-09-10T23:10:06.679Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1f/a2f3225c5ecf522684c1d051aea8ce8253f240e55be75826b787777afd6c/wrapt-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7e86fbc2ac8a363ea04abf631fad82720e16b17a25020f32dbe9b24a2ed2b0e3", size = 98890, upload-time = "2026-09-10T23:10:07.876Z" }, + { url = "https://files.pythonhosted.org/packages/68/6c/eb45660fd4d92cce11ec923f55bb2e647a6c18d30e53734eb07a3c530e31/wrapt-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24389748f0b9d5b67e478fad4fc8b3f1108422ef80716e48eead6cebcebbff08", size = 98742, upload-time = "2026-09-10T23:10:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/17a89a580cb0082e61b8375074d5c9e5d38e4aa83ee19b6174ee472d17c2/wrapt-2.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30d11c289b013bf384ff1a1a6553f150d0b855901708a9bef667a5680f8247c9", size = 236301, upload-time = "2026-09-10T23:10:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/01/ca/4700eb008a34bf02de328806ddde15fc84c8d1e65d3dcafb92a935a50319/wrapt-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9356dbb59199a0e4709de35fa4a1ac1a88ef6da99711a397f5b009233faff326", size = 237805, upload-time = "2026-09-10T23:10:12.594Z" }, + { url = "https://files.pythonhosted.org/packages/75/5d/26c1740299b29e190d5f4b4a99eb001401042a9d9ab338e3f8e1ce140ecb/wrapt-2.4.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8342f332dada211f64b74609e332d727b13315e9a83177f7918bf68c59f815f2", size = 217037, upload-time = "2026-09-10T23:10:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/3a/55/ec72991153a2ae8b40238bc44cec7c3ddf7706ef6e2d314b0c6f5c7febce/wrapt-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f86e328c482bc5383b4eda5094be0bed3617fc3076aa9225ff1a9eb6372de9b", size = 234659, upload-time = "2026-09-10T23:10:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/7cfa1e070dcda76ea56a3e252341cba1cd1e9412baf23cd7adfebf1114e2/wrapt-2.4.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4dc92697444ee380544fbb43c86612d8486529aadf917c22b5524141d4af074c", size = 214590, upload-time = "2026-09-10T23:10:16.744Z" }, + { url = "https://files.pythonhosted.org/packages/79/10/248841cb30107f6f32c53a02662e1c3e0c7c06bea0b8ebfaaee94885dcee/wrapt-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edd03758a7578526642508b8833d43496fdfba0f64e0025dfca153a7c1777735", size = 225103, upload-time = "2026-09-10T23:10:18.346Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/5b2bf7b35b008a39939a2908e85dba3b867596e535fc9f12ddf3ba1fcaf6/wrapt-2.4.1-cp312-cp312-win32.whl", hash = "sha256:5d83e412665aeb1e854eefbf1564d0d67872d9994b502a0bce96e6ff7f4970b7", size = 93441, upload-time = "2026-09-10T23:10:19.81Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2a/47be56772bfb07ef242d6e924049688e38384af2b2fc99b0f31180988448/wrapt-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:b4e7efdd476ac631a0181551fd9aace844765ea3ce2b5133b194fae4421e8ad0", size = 98808, upload-time = "2026-09-10T23:10:21.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/40e4c9735626afd1dc0eeb310310278361f192800503cda0f5b3d8d24db4/wrapt-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:38819761401baa2d11916d7265b82f23265f8fe5a31c431dd7c24a8863c65f88", size = 95240, upload-time = "2026-09-10T23:10:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1a/9b5aa3c2391aa6d00fffa085c2219e8fc91cd4d2b9b080d2b4e4b6b93f42/wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3", size = 98597, upload-time = "2026-09-10T23:10:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/fe/07/dc98150c2f9ee5b5fcbd841765e178ea6cc6c43733ab8d1f5181a4fb9f3d/wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145", size = 98842, upload-time = "2026-09-10T23:10:25.109Z" }, + { url = "https://files.pythonhosted.org/packages/08/c2/0e772a570e8d75c1b3ec45930a3023492fa68223f8f5e3485af4844c10c3/wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc", size = 234642, upload-time = "2026-09-10T23:10:26.644Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/4ce4d02dd95ff9ed972a2fc396d04fec636daa5a4ac18cc73a3dc20aa6c3/wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1", size = 235484, upload-time = "2026-09-10T23:10:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/436ef1df620ef8edd9ef057b4d55a0cd704435ff66852a0ef26cbaa02a58/wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d", size = 214371, upload-time = "2026-09-10T23:10:29.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/cc5b7503843399087db3106a7cf60d60d0900f69539e96148b9fff116291/wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697", size = 232347, upload-time = "2026-09-10T23:10:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/86/fb/17668b1ca572c44b458c09d76064d35f5f57d5ac7629248871604bef816c/wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73", size = 212792, upload-time = "2026-09-10T23:10:32.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2b/0c5de10d7259e07c478c44bf2fbe179c6878727d09edf0632b97884801db/wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4", size = 223585, upload-time = "2026-09-10T23:10:34.43Z" }, + { url = "https://files.pythonhosted.org/packages/aa/36/013dce1c687f8f1c87b1e78f403d04c80ae9b2ae77de4ddba16069a3e7d1/wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4", size = 93425, upload-time = "2026-09-10T23:10:35.901Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1d/d53bcf5910209a45191c8bc173ff0e00830416547d8038772dc444932ee0/wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca", size = 98569, upload-time = "2026-09-10T23:10:37.316Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/759d0c522918f9dfb620136622d8e1ce619570adda0de8fa2cfbb55cbb86/wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f", size = 95272, upload-time = "2026-09-10T23:10:38.657Z" }, + { url = "https://files.pythonhosted.org/packages/08/05/ed5aa8991c5e9969e2ca17f0e44eb324a9757f44fa982bfc13844cfe9e1d/wrapt-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6c35541cc729964c65c2b9b1f9cf317811039abc2da13b4557f20f21cdc292b", size = 98876, upload-time = "2026-09-10T23:10:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5f/f8bf07c3b9a2ad01d28d5ca419e28819bb16937d129167d97c446c05875d/wrapt-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6b9df84f0a96763159cccb8e3b0ed83cc950c7c8bf82d6e45428372a805b3224", size = 99054, upload-time = "2026-09-10T23:10:41.484Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/518e5f3ebd18472652e5332bf37728cd6634ff81a0aa568969d05cc66099/wrapt-2.4.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78af62413095d0a57077606654ce85e273deda8e2bfa28fdb04257b962d94ef2", size = 237430, upload-time = "2026-09-10T23:10:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/503835d183c1ca99268e0b5a98af8f487d528ebabb124f4003cd96e73b1f/wrapt-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60702ebc914d0bb01aa48f5c1785ceaaaa505e0a841b444a69d6ceb5de4097e", size = 237988, upload-time = "2026-09-10T23:10:44.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/a11a3e1eb18ae1d9adef1c39f8e8e36fd5b86db33938bebf8e6bc5fd9b06/wrapt-2.4.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8c4b44e4be7680fc496816e824b6ed134d781c4da296e44b75399196e5c248f1", size = 218554, upload-time = "2026-09-10T23:10:45.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/61/61a6f983737ba81008561ab634373bba25b986761d3a0f0aded8352ae3e8/wrapt-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fb5b3f94258bcf71db902795f4a71151c7a74d9fe21fffa00752d2bd286866f8", size = 235476, upload-time = "2026-09-10T23:10:47.528Z" }, + { url = "https://files.pythonhosted.org/packages/42/68/acb7cba46e0f662eaae421487807d2fed28ba52df9a00a3f04db16675e38/wrapt-2.4.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f1556a96b20d5bfdc9cb5dcd0a3ec65cbbac8a4eebcd3cd34efdc91c9519f660", size = 216454, upload-time = "2026-09-10T23:10:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/8b/52/3a4dab8d4803df595de82f19775535d28c036746340c1b498fc8ecba39aa/wrapt-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:094a606d0bf1c4b847b3a743d22fc69cb164015b8c70cf6f20a534d30889ed47", size = 225346, upload-time = "2026-09-10T23:10:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/d72d051757fb7955021d8174014679f951fd5067187d2faed95520e1f8d6/wrapt-2.4.1-cp314-cp314-win32.whl", hash = "sha256:b0ee076be124406a7f97ca663c4a3ba32b6bcdd9102ca82497feaca79e9cb33c", size = 93870, upload-time = "2026-09-10T23:10:52.495Z" }, + { url = "https://files.pythonhosted.org/packages/a1/00/1a1bf4d8b60b9e7ac009969595438549ae6e33b2e3e174b78d26a833aad3/wrapt-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:d2d6f9abaa52de05090a2b4a4c1d0e858a268c4de69eec277506af9fbcccff91", size = 98910, upload-time = "2026-09-10T23:10:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/1d/59/b3169c3bcdcff70a6d02ed2b6366097b083d31391d616c407efe9a943a82/wrapt-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:3152b2e94d733a9bd70dbd1c95f148266f079a70e9ffca2a9c5dc421ade1b4e6", size = 96013, upload-time = "2026-09-10T23:10:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/d29a063c2ca6e779305cd444dd5acaac60833d73b2d94c7b1d0820c27e3e/wrapt-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:37ff91b390400463ecd4d080ea823314510b6843ac53e6e35cc09bba1805d466", size = 102069, upload-time = "2026-09-10T23:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/7435de516099b528e2232770b459d6706002736f41d8f092bbb11ea01575/wrapt-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e0a518cac3e789443af54fc77f23e66f17d80192c281b160b42058f11fabccc5", size = 102647, upload-time = "2026-09-10T23:10:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a7/cca7fe0f26aa11cbbf09b2e6f4f774db167feeca43ad22fa1772afdccbad/wrapt-2.4.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ecdeb8a1ec13397421e6f925412186bd87ab86d63517e6825b6d7bccf781f26", size = 275614, upload-time = "2026-09-10T23:11:00.464Z" }, + { url = "https://files.pythonhosted.org/packages/77/22/f3b5f4428ae679b1e2c0d4833519045d8983a9df0ad24df96b195d44dd1b/wrapt-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15c4ede3fde08723cabab0a892d4b75d35b5a6f0a51c84e6542ac0bdc0507aa", size = 287064, upload-time = "2026-09-10T23:11:02.111Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b8/24274ec01b6ab6d0672f4312bfa29b5f1ffd9d56f16c8da67d5683883956/wrapt-2.4.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e32c5951c36fed88b6c603dc0bab209a62dc3217bd8762413634725fe28f2f3c", size = 255440, upload-time = "2026-09-10T23:11:03.746Z" }, + { url = "https://files.pythonhosted.org/packages/8b/df/c63a36f0f03d70c7b92f60ec1ad3757088167914e1a262f5dcc7df233d51/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:898513db90d55a4ed3009312d41c12932b8163edbae3535280046d47aaff774f", size = 281681, upload-time = "2026-09-10T23:11:05.41Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c7/4930b6a369d9b3da59f5ada4ae9e659e2d4a68fe72c5d1ce15e5bead2d24/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:42d01574bd4bcafc3476e77a95c6c0dd6167101991401325fc5591e2db21a91d", size = 252409, upload-time = "2026-09-10T23:11:06.867Z" }, + { url = "https://files.pythonhosted.org/packages/68/d7/f6bc5703061598c4a2ad4f7a2efce696782be6e9d5afed714a59f5902ead/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:68a403adbeb4dd2654d6d108e43e69f0f90e6d34cb7588e0e6589109f6985e67", size = 270559, upload-time = "2026-09-10T23:11:08.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/a3/952337a153e634f530079334c47dcc935b20f8ea22f364729fc2a8cf1821/wrapt-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:7cb3035b332bc9d21478600ba82c7c71e2d074ed9b4e76364297f54598792227", size = 96241, upload-time = "2026-09-10T23:11:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/48f0651547a125dd84e312b86268d4150c565a29f81ff9ca6fd81eba4be4/wrapt-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:707d2bef68deddd0fc74a81103d91b286a69ac5a9ff0f0a6dad66f8787e86697", size = 102655, upload-time = "2026-09-10T23:11:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/977441bed5e5afbffe7a789067ac2f5fd2e9f848884122a23db9ccae6a70/wrapt-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28cb1c2713b4377bf03ddcb3e76d8216e4bb334199066c6122be7eed2f72de8c", size = 98495, upload-time = "2026-09-10T23:11:12.952Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1c/03bcbc3dbf6ac11231f0434f2494aa7b80657b130be1a1a639192a3e44c9/wrapt-2.4.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:96a5023fa63ca2f095f7776c8bfaa1694547577f762646c375abafd8f8ae649b", size = 98878, upload-time = "2026-09-10T23:11:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/87/26/5ad9bd421b8cdce0b2227a61f09586423765da3c284f8bbf0f69fc149fdf/wrapt-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b21924949dedc3ac63725e09b9b0a130e771975f03432789344ed3422c46008", size = 99090, upload-time = "2026-09-10T23:11:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c7/fa768261966d587650004ac37bef541540014b1fe23fbf4919c497ebc98c/wrapt-2.4.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2f725af353bb3319c528ee69dbff838599426974288b281a3258d5397b18cb63", size = 237803, upload-time = "2026-09-10T23:11:17.738Z" }, + { url = "https://files.pythonhosted.org/packages/0f/37/9c1c876bd88fef8d065b3e8bf125cad9291d70ed3695cedb41b36b2453ec/wrapt-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20327e162ef7953fae56b31a43ca457f94ed1fc7a206d01e3d5c8412d1b91572", size = 238338, upload-time = "2026-09-10T23:11:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/91/5b/47b2f2f08b90d9d3b4e1790ccae0d4e4de40e73f6614c9a52465a04f02a5/wrapt-2.4.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2cedb743dbdfb9b6d4f11acd8cb6329460264429777e81ea2e86b1b72ea503af", size = 220668, upload-time = "2026-09-10T23:11:21.422Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/da7844c7a3f7c01f3496e690e15ce9d01c0949278c64d4287ec472c90f72/wrapt-2.4.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5a54744b1193505f19016194979b773ee3754a199e9ad90db18f2e9a18fffa16", size = 235812, upload-time = "2026-09-10T23:11:23.211Z" }, + { url = "https://files.pythonhosted.org/packages/69/af/ac4aa9f795721a54a21bac329201f7df1b199b71ef064187a266d4d29836/wrapt-2.4.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b0d38d9cc23e9781e6584f303e5c5a9f0d45b85de92ad45678c4dde8d49d9535", size = 218195, upload-time = "2026-09-10T23:11:24.983Z" }, + { url = "https://files.pythonhosted.org/packages/78/aa/cff7670ec1cfa75b951171cfb41d45c7bdb94e928620a1599ec2b00cd2dd/wrapt-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a524ca32f0bcdde2b728d9f81f9527d4dd24b13f15d180f05d08cdc01d1cebe0", size = 225713, upload-time = "2026-09-10T23:11:26.576Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/64f138aef50b5ea1dfacca2a023b32ba3e41ef087bbd92e8997a95f4a0b8/wrapt-2.4.1-cp315-cp315-win32.whl", hash = "sha256:47c267617551e906de72f6e7265aa3bce84c63d44513d2d2e735e943422aa0a2", size = 93880, upload-time = "2026-09-10T23:11:28.253Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/afe7484a0f7232e87f2ebb65286c1025d43cbe1de4e4051b127b024a92f7/wrapt-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:ee437bd7fd050823ef731aad20e968ca8fe670b95e1841f5d201bfdbad4a4e96", size = 98919, upload-time = "2026-09-10T23:11:29.832Z" }, + { url = "https://files.pythonhosted.org/packages/83/21/6865f976c6a1082ca61e2792bc6a2d6a0ed03cd750a46f837753127191f8/wrapt-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:ebf3b703752b53366fd02b7fbd8c447428e0349c9af3fc17c7a05076dbdd749a", size = 96021, upload-time = "2026-09-10T23:11:31.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/48/baf784cc9f1fe554f96daf15af06c7c466d76469d5540e357cf564755606/wrapt-2.4.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0f5990f5db090f069fcd577cc61cc8d463db73cde132abba9033b0a264fc06d0", size = 102028, upload-time = "2026-09-10T23:11:33.102Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/fb809c95bf5512196aeda4c640aabf4e92f607340b8b2556555690a52b7a/wrapt-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef19a2590b195ac294deff8ee350a027a479afdd2ef2170ce3900af92406e110", size = 102640, upload-time = "2026-09-10T23:11:35.145Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/bd462010ccbbe298479f320bf2bb02996706003e9c20a15790b65a186f8b/wrapt-2.4.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:432f402f9b6014403cacf9fd18a6bf089c77964330eae951a155131ab0414f5d", size = 275999, upload-time = "2026-09-10T23:11:36.863Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/bd043b2bad2943336e090583ff6cea2f3e8776bde02f073ccf760b16eee1/wrapt-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb7c0f8bdd21e954bad89d554cfb7431c2f8179842853f199841ab90cbebe914", size = 287576, upload-time = "2026-09-10T23:11:38.79Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/9937bbd61ca4e7f58a7e8a38f5ff3562334f741de51b87f0e031df441254/wrapt-2.4.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e82a1669d63b79a2b53041bbb6ea096b5763cab3ca698ed7ac244b3acb7cd51", size = 256763, upload-time = "2026-09-10T23:11:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0f78cc7e7fc6313cdba32df06f4b29a45a4a2aed3499040d6c1628d3f339/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b19e71c914c435d2c5652caea386c9bf2807979e957183f5bb06d5ed5fa8b55e", size = 282244, upload-time = "2026-09-10T23:11:42.483Z" }, + { url = "https://files.pythonhosted.org/packages/be/0d/30317d738b9898a7fbedc193657f151a468b0fa7235f4abd58a646a99cd8/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a1e4870d3368c6c918f38e308d5dcea970ea5b908ce889c21412f3a517ebf0c3", size = 254153, upload-time = "2026-09-10T23:11:44.357Z" }, + { url = "https://files.pythonhosted.org/packages/12/53/fe1f251a89f471ca7c5e17f2edbd8efd48fea6f1c08430dcf60986ea4183/wrapt-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2286e8e4a937966706463d1bcea30dfefee529e6e73e097a18e9e066a07ae6c2", size = 271234, upload-time = "2026-09-10T23:11:46.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7c/d63758cd7fa0d18c415a87312a07cdd9f0102eb824aaab5c7450b3ec08e5/wrapt-2.4.1-cp315-cp315t-win32.whl", hash = "sha256:80afa3b7010e82899044a2a189c468a0cd8c89980398a59ba3b96b451a6dc5e9", size = 96236, upload-time = "2026-09-10T23:11:47.942Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2e/1785e6d2696db08b07c053b1f4ccb2b5e8f9d12e9bf2c7497693be6b4504/wrapt-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3593b43fabab6b59fe77e38f7e40974aae120c30cea3fd1ceaea6621ee96be00", size = 102662, upload-time = "2026-09-10T23:11:49.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fd/3f4ac5c4754948355e050f952ae7a64cdca94891e49fe9927c3f90a73334/wrapt-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ac1939ccf3e1c33f463706fbf52db5075f5eefb6042bcc1f9cf48c2c20ef478c", size = 98500, upload-time = "2026-09-10T23:11:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a2/edcfc8d9a30375791b775715f8501364588f65b494ec4f6930568a19e765/wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c", size = 75317, upload-time = "2026-09-10T23:12:14.82Z" }, ] [[package]] @@ -6421,118 +6539,152 @@ wheels = [ [[package]] name = "yarl" -version = "1.24.5" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ac/cacdda1f0a90441297210bc34cf7e4ac1b7318c8030ebd83bdf6fe82f1db/yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750", size = 135466, upload-time = "2026-07-20T02:04:21.695Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a5/1b2ceace0230e40c52ab1b263148059a43a6303219b996affc68f8381836/yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2", size = 97291, upload-time = "2026-07-20T02:04:24.045Z" }, - { url = "https://files.pythonhosted.org/packages/59/1d/340d1a0db7bbce1f291afc044255ebf4ebbce2b25ab1b3f7d3d069080f5d/yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871", size = 97154, upload-time = "2026-07-20T02:04:25.761Z" }, - { url = "https://files.pythonhosted.org/packages/05/41/25596a33c2fb5098dca8dc3773b04221db64ded0b7f8f09885647d864610/yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0", size = 109196, upload-time = "2026-07-20T02:04:27.543Z" }, - { url = "https://files.pythonhosted.org/packages/f2/df/dd9f2fb8a5c6054fbefd1538d2b9b1127e612d2ee64b307a070173b57afd/yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e", size = 102556, upload-time = "2026-07-20T02:04:29.16Z" }, - { url = "https://files.pythonhosted.org/packages/cb/57/4754b9d2c8945880290ecba0864e8b0441e117bba70534fe819e3645e174/yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2", size = 117965, upload-time = "2026-07-20T02:04:30.845Z" }, - { url = "https://files.pythonhosted.org/packages/74/b5/6a9ece27d2043c3386f902dd078ab35d29ef5126b3206ebffb673283a7cb/yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621", size = 116266, upload-time = "2026-07-20T02:04:32.573Z" }, - { url = "https://files.pythonhosted.org/packages/9e/bc/a6653249f6ee59ec85dcfec008d9cbc16586dad613963bb17a91b2b993a5/yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba", size = 110758, upload-time = "2026-07-20T02:04:34.235Z" }, - { url = "https://files.pythonhosted.org/packages/65/7e/c5a12fb8208df7b981bc82256e7831ce428eeaf893f7bbe6179c57bb9252/yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950", size = 110120, upload-time = "2026-07-20T02:04:35.85Z" }, - { url = "https://files.pythonhosted.org/packages/04/6c/1b659b964626694667b3ec01bf4bcff564b73ae7c48ea1fbfe588b78b461/yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00", size = 108834, upload-time = "2026-07-20T02:04:37.67Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/bf48f55c2104e40c15b7b13fad0a5756a11552a55f01c90bc90a66ab81c3/yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed", size = 103442, upload-time = "2026-07-20T02:04:39.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/ac/84b273ac133ecdce598fc1f4140a08a1bf2044048bff8106371d207d105f/yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440", size = 117413, upload-time = "2026-07-20T02:04:41.549Z" }, - { url = "https://files.pythonhosted.org/packages/a4/55/9307e03977d3b290dfa42e5d2bae7b6140808fd1786fbe70cd9d3bee53c5/yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1", size = 109498, upload-time = "2026-07-20T02:04:43.468Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/791a6f314cb4c989c19f8e3a10271f1e469c077143915e52474d80f26b4b/yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6", size = 116062, upload-time = "2026-07-20T02:04:45.319Z" }, - { url = "https://files.pythonhosted.org/packages/19/1a/ddd3807b86055010e2f99aa89b3c640effdb65696766c20597f696f48a1c/yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d", size = 110941, upload-time = "2026-07-20T02:04:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/6d/03/f34271bba042d2187508bf62aea20a14129efb5a1acfc6a2efe7544630b4/yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224", size = 97534, upload-time = "2026-07-20T02:04:48.774Z" }, - { url = "https://files.pythonhosted.org/packages/e4/02/ecc8dc31b9f355731e700f8402b8075d2ea1737dbc4baf4abf0f0fc64288/yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13", size = 93603, upload-time = "2026-07-20T02:04:50.686Z" }, - { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, - { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, - { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, - { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, - { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, - { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, - { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, - { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, - { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, - { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, - { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, - { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, - { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, - { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, - { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, - { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, - { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, - { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, - { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, - { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, - { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, - { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, - { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, - { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, - { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, - { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, - { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, - { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, - { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, - { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, - { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, - { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, - { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, - { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, - { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, - { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, - { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, - { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, - { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, - { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, - { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, - { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, - { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] [[package]] From bf126f38cf2c4b116be32f8687388ddcefa467f1 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Thu, 1 Oct 2026 17:25:24 -0600 Subject: [PATCH 26/65] aws - sagemaker-endpoint - add metrics filter (#11078) --------- Co-authored-by: Claude Opus 5 --- .gitignore | 1 + c7n/data/sagemaker_metrics.yaml | 121 ++++ c7n/filters/metrics.py | 34 + c7n/resources/sagemaker.py | 448 ++++++++++++ docs/source/aws/examples/sagemakermetrics.rst | 188 +++++ .../api.sagemaker.DescribeEndpoint_1.json | 104 +++ .../api.sagemaker.DescribeEndpoint_2.json | 58 ++ .../api.sagemaker.DescribeEndpoint_3.json | 43 ++ .../api.sagemaker.DescribeEndpoint_4.json | 43 ++ .../api.sagemaker.ListEndpoints_1.json | 108 +++ ...i.sagemaker.ListInferenceComponents_1.json | 64 ++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../api.sagemaker.ListTags_3.json | 7 + .../api.sagemaker.ListTags_4.json | 7 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../runtime.sagemaker.InvokeEndpoint_1.json | 13 + .../runtime.sagemaker.InvokeEndpoint_2.json | 13 + .../runtime.sagemaker.InvokeEndpoint_3.json | 13 + .../runtime.sagemaker.InvokeEndpoint_4.json | 13 + .../runtime.sagemaker.InvokeEndpoint_5.json | 13 + .../api.sagemaker.DescribeEndpoint_1.json | 104 +++ .../api.sagemaker.DescribeEndpoint_2.json | 58 ++ .../api.sagemaker.DescribeEndpoint_3.json | 43 ++ .../api.sagemaker.DescribeEndpoint_4.json | 43 ++ .../api.sagemaker.ListEndpoints_1.json | 108 +++ ...i.sagemaker.ListInferenceComponents_1.json | 64 ++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../api.sagemaker.ListTags_3.json | 7 + .../api.sagemaker.ListTags_4.json | 7 + .../monitoring.GetMetricStatistics_1.json | 23 + .../api.sagemaker.DescribeEndpoint_1.json | 104 +++ .../api.sagemaker.DescribeEndpoint_2.json | 58 ++ .../api.sagemaker.DescribeEndpoint_3.json | 43 ++ .../api.sagemaker.DescribeEndpoint_4.json | 43 ++ .../api.sagemaker.ListEndpoints_1.json | 108 +++ ...i.sagemaker.ListInferenceComponents_1.json | 64 ++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../api.sagemaker.ListTags_3.json | 7 + .../api.sagemaker.ListTags_4.json | 7 + .../monitoring.GetMetricStatistics_1.json | 23 + .../runtime.sagemaker.InvokeEndpoint_1.json | 13 + .../runtime.sagemaker.InvokeEndpoint_2.json | 13 + .../runtime.sagemaker.InvokeEndpoint_3.json | 13 + .../runtime.sagemaker.InvokeEndpoint_4.json | 13 + .../runtime.sagemaker.InvokeEndpoint_5.json | 13 + .../api.sagemaker.DescribeEndpoint_1.json | 104 +++ .../api.sagemaker.DescribeEndpoint_2.json | 58 ++ .../api.sagemaker.DescribeEndpoint_3.json | 43 ++ .../api.sagemaker.DescribeEndpoint_4.json | 43 ++ .../api.sagemaker.ListEndpoints_1.json | 108 +++ ...i.sagemaker.ListInferenceComponents_1.json | 64 ++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../api.sagemaker.ListTags_3.json | 7 + .../api.sagemaker.ListTags_4.json | 7 + .../monitoring.GetMetricStatistics_1.json | 23 + .../api.sagemaker.DescribeEndpoint_1.json | 104 +++ .../api.sagemaker.DescribeEndpoint_2.json | 58 ++ .../api.sagemaker.DescribeEndpoint_3.json | 43 ++ .../api.sagemaker.DescribeEndpoint_4.json | 43 ++ .../api.sagemaker.ListEndpoints_1.json | 108 +++ ...i.sagemaker.ListInferenceComponents_1.json | 64 ++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../api.sagemaker.ListTags_3.json | 7 + .../api.sagemaker.ListTags_4.json | 7 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 8 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 8 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 + .../monitoring.GetMetricStatistics_3.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../runtime.sagemaker.InvokeEndpoint_1.json | 13 + .../runtime.sagemaker.InvokeEndpoint_10.json | 13 + .../runtime.sagemaker.InvokeEndpoint_11.json | 13 + .../runtime.sagemaker.InvokeEndpoint_12.json | 13 + .../runtime.sagemaker.InvokeEndpoint_13.json | 13 + .../runtime.sagemaker.InvokeEndpoint_14.json | 13 + .../runtime.sagemaker.InvokeEndpoint_15.json | 13 + .../runtime.sagemaker.InvokeEndpoint_2.json | 13 + .../runtime.sagemaker.InvokeEndpoint_3.json | 13 + .../runtime.sagemaker.InvokeEndpoint_4.json | 13 + .../runtime.sagemaker.InvokeEndpoint_5.json | 13 + .../runtime.sagemaker.InvokeEndpoint_6.json | 13 + .../runtime.sagemaker.InvokeEndpoint_7.json | 13 + .../runtime.sagemaker.InvokeEndpoint_8.json | 13 + .../runtime.sagemaker.InvokeEndpoint_9.json | 13 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../monitoring.GetMetricStatistics_1.json | 23 + .../sagemaker_endpoint_metrics/README.md | 48 ++ .../sagemaker_endpoint_metrics/main.tf | 300 ++++++++ .../sagemaker_endpoint_metrics/model.tar.gz | Bin 0 -> 587 bytes .../probe_metrics.py | 321 +++++++++ .../tf_resources.json | 514 ++++++++++++++ tests/test_sagemaker.py | 648 +++++++++++++++++- 153 files changed, 6593 insertions(+), 1 deletion(-) create mode 100644 c7n/data/sagemaker_metrics.yaml create mode 100644 docs/source/aws/examples/sagemakermetrics.rst create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListInferenceComponents_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_5.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListInferenceComponents_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListInferenceComponents_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_5.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListInferenceComponents_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListInferenceComponents_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_3.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_4.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_10.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_11.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_12.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_13.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_14.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_15.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_3.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_4.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_5.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_6.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_7.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_8.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_9.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_ConcurrentRequestsPerCopy/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_DiskUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilizationNormalized/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_GPUReservation/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_InvocationModelErrors/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_Invocations/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_InvocationsPerCopy/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_MemoryReservation/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_MemoryUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_ModelLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_inference_component_OverheadLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/terraform/sagemaker_endpoint_metrics/README.md create mode 100644 tests/terraform/sagemaker_endpoint_metrics/main.tf create mode 100644 tests/terraform/sagemaker_endpoint_metrics/model.tar.gz create mode 100644 tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py create mode 100644 tests/terraform/sagemaker_endpoint_metrics/tf_resources.json diff --git a/.gitignore b/.gitignore index 7775c2c574c..7683d3e24d7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .eggs/ .terraform .terraform.lock.hcl +*.tfstate* # c7n/version.py src/ deps/ diff --git a/c7n/data/sagemaker_metrics.yaml b/c7n/data/sagemaker_metrics.yaml new file mode 100644 index 00000000000..b427f47a685 --- /dev/null +++ b/c7n/data/sagemaker_metrics.yaml @@ -0,0 +1,121 @@ +# The metrics SageMaker publishes, and the dimensions that identify them. +# +# Maintained by hand/agent. +# See tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py +# +# Each section names the metric table it came from. The namespace is in the +# prose above that table. A dimension entry is one row of the following +# dimensions table: the names identifying a single time series. +# +# kind names a variety of the resource, for metrics only some varieties +# publish; leave it out when every variety publishes the section's metrics. +# Endpoints come in two kinds: a "classic" endpoint attaches a model to each +# production variant, while an "inference-component" endpoint carries an +# execution role and hosts models as components placed on a variant. The two +# report invocations under different dimensions, which the documentation +# lists together without saying which applies when. +# +# Only the metrics of resources c7n filters on are listed; the page also +# covers jobs, Ground Truth, Feature Store and pipelines. +# +# The documentation's dimensions tables say which dimensions a metric can be +# filtered by, not which sets exist, and CloudWatch identifies a series by +# the exact set. Only sets observed to carry data are listed here -- see +# tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py. Notably +# instance type is published only alongside AvailabilityZone and Region, so +# the documented "EndpointName, VariantName, InstanceType" is absent. + +- title: Endpoint metrics + resource: sagemaker-endpoint + namespace: /aws/sagemaker/Endpoints + metrics: + - CPUUtilization + - DiskUtilization + - GPUMemoryUtilization + - GPUUtilization + - MemoryUtilization + dimensions: + - EndpointName, VariantName + +# Reservations exist only where a component reserves part of an instance, +# and the normalized metrics report utilization against them. Observed for a +# component-hosting endpoint and not for a classic one. +- title: Endpoint metrics + resource: sagemaker-endpoint + kind: inference-component + namespace: /aws/sagemaker/Endpoints + metrics: + - CPUReservation + - CPUUtilizationNormalized + - GPUMemoryUtilizationNormalized + - GPUReservation + - GPUUtilizationNormalized + - MemoryReservation + dimensions: + - EndpointName, VariantName + +- title: Endpoint invocation metrics + resource: sagemaker-endpoint + kind: classic + namespace: AWS/SageMaker + metrics: + - ConcurrentRequestsPerModel + - Invocation4XXErrors + - Invocation5XXErrors + - InvocationModelErrors + - Invocations + - InvocationsPerInstance + - ModelLatency + - ModelSetupTime + - OverheadLatency + - MidStreamErrors + - FirstChunkLatency + - FirstChunkModelLatency + - FirstChunkOverheadLatency + dimensions: + - EndpointName, VariantName + +# A component-hosting endpoint reports its invocations per component, with +# no EndpointName dimension. +- title: Endpoint invocation metrics + resource: sagemaker-endpoint + kind: inference-component + namespace: AWS/SageMaker + metrics: + - ConcurrentRequestsPerCopy + - Invocation4XXErrors + - Invocation5XXErrors + - InvocationModelErrors + - Invocations + - InvocationsPerCopy + - ModelLatency + - ModelSetupTime + - OverheadLatency + - MidStreamErrors + - FirstChunkLatency + - FirstChunkModelLatency + - FirstChunkOverheadLatency + dimensions: + - InferenceComponentName + +- title: Multi-model endpoint model loading metrics + resource: sagemaker-endpoint + kind: classic + namespace: AWS/SageMaker + metrics: + - ModelLoadingWaitTime + - ModelUnloadingTime + - ModelDownloadingTime + - ModelLoadingTime + - ModelCacheHit + dimensions: + - EndpointName, VariantName + +- title: Multi-model endpoint model instance metrics + resource: sagemaker-endpoint + kind: classic + namespace: /aws/sagemaker/Endpoints + metrics: + - LoadedModelCount + dimensions: + - EndpointName, VariantName diff --git a/c7n/filters/metrics.py b/c7n/filters/metrics.py index 6763f3cd633..4156c4c062f 100644 --- a/c7n/filters/metrics.py +++ b/c7n/filters/metrics.py @@ -70,6 +70,9 @@ class MetricsFilter(Filter): Rather than skipping those resources, "missing-value: 0" causes the policy to treat their request counts as 0. + Missing values only make sense when the statistic represents a + total, such as a sum or count. + Note the default statistic for metrics is Average. The ``period-start`` key allows you to align the metric window in two ways. @@ -89,7 +92,38 @@ class MetricsFilter(Filter): value: 30 op: less-than period-start: start-of-day + + Cloudwatch metrics are time series identified by a metric name, + a metric namespace, and by key-value pairs called "dimensions". A + "dimension set" identifies the possible keys and is often as + simple as a single element that is a resource identifier, like + BucketName. + + Metric filters automatically supply a dimension that is the resource + identifier. + + Additional dimensions can be provided to narrow the metric data + used: + + .. code-block:: yaml + + dimensions: + StorageType: StandardStorage """ + # + # Note: metric filters are implemented by: + # + # - enumerating resources for a resource type. + # + # - For each resource: + # + # - Fetch the metric for the resource, identified by the metric + # name, namespace, and resource dimensions. + # + # - Evaluate the condition against the data. + # The filter matches if the condition holds for every data point. + # If there are no data, the filter doesn't match. + # A missing value can be supplied to be used when there are no data. schema = type_schema( 'metrics', diff --git a/c7n/resources/sagemaker.py b/c7n/resources/sagemaker.py index 222124ac96c..c29acb6c8c1 100644 --- a/c7n/resources/sagemaker.py +++ b/c7n/resources/sagemaker.py @@ -1,16 +1,109 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import collections.abc +import functools +import importlib.resources +import typing + +import yaml + from c7n.actions import BaseAction +from c7n.exceptions import PolicyValidationError from c7n.manager import resources from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, ConfigSource from c7n.utils import local_session, type_schema, QueryParser from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment from c7n.filters.vpc import SubnetFilter, SecurityGroupFilter, NetworkLocation from c7n.filters.kms import KmsRelatedFilter +from c7n.filters.metrics import MetricsFilter from c7n.filters.offhours import OffHour, OnHour +DimensionName = str +DimensionNames = collections.abc.Iterable[str] + + +class Dimension(typing.TypedDict): + Name: DimensionName + Value: str + + +ResourceTypename = str +MetricName = str +Namespace = str + + +class PublishedMetricInfo(typing.TypedDict): + dimension_sets: list[list[DimensionName]] + namespace: Namespace + + +# A variety of a resource that publishes some metrics the other varieties +# don't. None for metrics every variety publishes. +Kind = typing.Optional[str] + +SAGEMAKER_DIMENSION_SETS: dict[ + ResourceTypename, + dict[MetricName, set[tuple[DimensionName, ...]]]] = None +SAGEMAKER_METRICS: dict[Kind, dict[ResourceTypename, dict[MetricName, PublishedMetricInfo]]] = None + + +def load_sagemaker_metrics(): + """Expand data/sagemaker_metrics.yaml into a lookup. + + By resource, kind and metric name. The file groups metrics by the + documentation table they came from instead. + """ + global SAGEMAKER_DIMENSION_SETS, SAGEMAKER_METRICS + + SAGEMAKER_METRICS = metrics = {} + SAGEMAKER_DIMENSION_SETS = dimension_sets = {} + + sections = yaml.safe_load( + (importlib.resources.files('c7n') / 'data/sagemaker_metrics.yaml' + ).read_text()) + + # Collect data by kind + for section in sections: + section_dimension_sets = set( + tuple(name.strip() for name in dimensions.split(',')) + for dimensions in section['dimensions'] + ) + published: PublishedMetricInfo = { + 'namespace': section['namespace'], + 'dimension_sets': section_dimension_sets, + } + kind = section.get('kind') + table = metrics.setdefault(kind, {}).setdefault(section['resource'], {}) + for metric in section['metrics']: + if metric in table: + raise AssertionError( + f"{metric} is in more than one {section['resource']}" + f" {kind} section of sagemaker_metrics.yaml") + table[metric] = published + + # Accumulate dimension_sets + table = dimension_sets.setdefault(section['resource'], {}) + for metric in section['metrics']: + table.setdefault(metric, set()).update(section_dimension_sets) + + # Broadcast data from None to individual kinds + if all_kinds := metrics.get(None): + for kind in metrics: + if kind is not None: + for resource in all_kinds: + table = metrics.setdefault(kind, {}).setdefault(resource, {}) + resource_metrics = all_kinds[resource] + for metric in resource_metrics: + if metric in table: + raise AssertionError(f"Duplicate metric for {kind} {resource} {metric}") + table[metric] = resource_metrics[metric] + + +load_sagemaker_metrics() + + class NotebookDescribe(DescribeSource): def augment(self, resources): @@ -360,6 +453,16 @@ class resource_type(TypeInfo): date = 'CreationTime' cfn_type = 'AWS::SageMaker::Endpoint' + # Metrics: + dimension = 'EndpointName' + + # This is right except when it's AWS/SageMaker or + # /aws/sagemaker/InferenceComponents. This gets overridden by + # SagemakerMetricsFilter. MetricsFilter wants something set + # here or in filter data and making users specify it in filter + # data is mean. + metrics_namespace = '/aws/sagemaker/Endpoints' + permissions = ('sagemaker:ListTags',) source_mapping = {'describe': EndpointDescribe} @@ -368,6 +471,351 @@ class resource_type(TypeInfo): SagemakerEndpoint.filter_registry.register('marked-for-op', TagActionFilter) +class SageMakerMetricsFilter(MetricsFilter): + """Filter SageMaker resources on their metrics + + See the MetricsFilter doc string and + docs/source/aws/examples/sagemakermetrics.rst + """ + + @functools.cached_property + def resource_dimension_name(self) -> DimensionName: + # not self.model, which the base filter only sets once it runs + return self.manager.get_model().dimension + + def resource_kind(self, _) -> Kind: + return None + + def published_dimension_sets(self) -> set[DimensionNames]: + """Every set this metric can be dimensioned by, whatever the kind. + + What a policy may ask for. The data is maintained by hand from the + aws documentation, so a name missing from it is either a typo or a + metric aws has published since -- see data/sagemaker_metrics.yaml. + """ + try: + return SAGEMAKER_DIMENSION_SETS[self.manager.type][self.data['name']] + except KeyError: + raise AssertionError( + f"no documented {self.manager.type} metric named" + f" {self.data['name']}") + + def resource_published_metric( + self, resource) -> typing.Optional[PublishedMetricInfo]: + """Return the published metric for the resource kind. + + We don't expect most resources to have different kinds, but + endpoint do. None when this kind doesn't publish the metric: the + resource has no series, rather than the policy being wrong, which + published_dimension_sets decides. + """ + return ( + SAGEMAKER_METRICS + [self.resource_kind(resource)] + [self.manager.type] + .get(self.data['name']) + ) + + def can_enumerate_dimension(self, _) -> bool: + """Can we enumerate this dimention for a given resource. + """ + return False + + @functools.cached_property + def resource_dimension_derived_value(self) -> typing.Optional[str]: + """Return the value for the resource dimension name + + Derived from given dimension when the resource identifier isn't + available as one of the dimensions used. + """ + + def _can_use_dimension_names( + self, dimension_names: DimensionNames + ) -> bool: + given_dimensions = self.data.get('dimensions') + given_dimenion_names = set(given_dimensions or ()) + resource_dimension_name = self.resource_dimension_name + + # Any free dimensions must be enumerable: + free_dimensions = ( + set(dimension_names) + - given_dimenion_names + - {resource_dimension_name} + ) + if not all( + self.can_enumerate_dimension(dimension_name) + for dimension_name in free_dimensions + ): + return False + + # Are all given dimensions present? + if given_dimenion_names - set(dimension_names): + return False + + # Is the resource dimension name present: + if resource_dimension_name in dimension_names: + return True + + # or it can be derived + return ( + # Through enumeration: + any( + self.can_enumerate_dimension(dimension_name) + for dimension_name in dimension_names + ) + # Or via application of given dimensions + or self.resource_dimension_derived_value is not None + ) + + # keys of the shared schema this filter doesn't implement, rather than + # accepting and ignoring them + unsupported = ('percent-attr', 'attr-multiplier') + + def validate(self): + super().validate() + + # Check that we have usable dimension sets for given dimensions + if not any( + self._can_use_dimension_names(dimension_names) + for dimension_names in self.published_dimension_sets() + ): + raise PolicyValidationError( + f"metrics filter on {self.manager.type} can't use dimensions" + f" {sorted(self.data.get('dimensions', ()))}" + f" for {self.data['name']}") + + # fail on an undocumented metric name while the policy is being + # loaded, rather than on an empty report later + if 'namespace' in self.data: + raise PolicyValidationError( + f"metrics filter on {self.manager.type} determines the" + " namespace from the metric name; remove the namespace") + for key in self.unsupported: + if key in self.data: + raise PolicyValidationError( + f"metrics filter on {self.manager.type} doesn't" + f" support {key}") + + def get_resource_dimension_names(self, resource) -> typing.Optional[DimensionNames]: + """Get dimension names to get dimension sets for a resource + """ + metric = self.resource_published_metric(resource) + if metric is None: + # this kind of resource doesn't publish it, so it has no series + return None + usable = sorted( + filter(self._can_use_dimension_names, metric["dimension_sets"]), + key=lambda dimension_set: len(dimension_set) + ) + if usable: + return usable[0] + + def get_dimensions_set(self, resource) -> list[dict[str, str]]: + """The dimensions set naming a resource's metrics + + This is a list of dicts, which is simpler and saner that + lists of lists of dimensions. We convert to lists of lists of + Dimensions when we make API requests + """ + + dimension_names = self.get_resource_dimension_names(resource) + if dimension_names is None: + return [] + + base_dimensions = dict(self.data.get('dimensions', {})) + resource_dimension_name = self.resource_dimension_name + resource_dimension_value = resource[resource_dimension_name] + + if resource_dimension_name in dimension_names: + if resource_dimension_name in base_dimensions: + if base_dimensions[resource_dimension_name] != resource_dimension_value: + # given dimensions named some other resource + return [] + else: + base_dimensions[resource_dimension_name] = resource_dimension_value + else: + if len(base_dimensions) == len(dimension_names): + # There are no free dimensions. Check the resource + # dimension value derived from given values. + if self.resource_dimension_derived_value != resource_dimension_value: + return [] + + result = [base_dimensions] + for dimension_name in dimension_names: + if dimension_name not in base_dimensions: + result = [ + dict(result_dimension, **{dimension_name: dimension_value}) + for result_dimension in result + for dimension_value in self.enumerate_dimension(dimension_name, resource) + ] + + return result + + def get_resource_namespace(self, resource): + metric = self.resource_published_metric(resource) + return metric["namespace"] + + def get_resource_metrics(self, client, resource, extended_statistics): + """Yield time series for each of our metrics. + + In SageMaker, metric data are spread over multiple metrics + (time series). Each metric is identified by the metric name, + namespace, and a collection of Dimensions. + + This is implemented as a generator, so the caller can stop + early if the filter condition isn't met. + """ + + dimensions_set = self.get_dimensions_set(resource) + if not dimensions_set: + return + + namespace = self.get_resource_namespace(resource) + # the window, not just its length: period-start moves start and end + # without changing days or period + base_key = ( + f"{namespace}" + f".{self.metric}" + f".{self.statistics}" + f".{self.start.isoformat()}" + f".{self.end.isoformat()}" + f".{self.period}" + ) + base_params = dict( + Namespace=namespace, + MetricName=self.metric, + StartTime=self.start, + EndTime=self.end, + Period=self.period, + **{ + 'ExtendedStatistics' if extended_statistics else 'Statistics': + [self.statistics] + } + ) + cache = resource.setdefault('c7n.metrics', {}) + for dimensions in dimensions_set: + dimension_key = '.'.join( + f"{k}={v}" + for k, v in sorted(dimensions.items()) + ) + cache_key = f"{base_key}.{dimension_key}" + cached = cache.get(cache_key) + if cached is not None: + yield cached + else: + params = dict( + base_params, + Dimensions=[dict(Name=k, Value=v) for k, v in dimensions.items()]) + points = self.get_metric_data(client, params) + if extended_statistics: + points = [p['ExtendedStatistics'] for p in points] + cache[cache_key] = points + yield points + + def process_resource_set(self, resource_set): + client = local_session( + self.manager.session_factory).client('cloudwatch') + extended_statistics = self.statistics not in self.standard_stats + matched = [] + for resource in resource_set: + empty = True + for points in self.get_resource_metrics(client, resource, extended_statistics): + if points: + empty = False + if any(not self.op(point[self.statistics], self.value) + for point in points + ): + # We failed to match a point, so bail + break + else: + if empty: + # There weren't any data, so the missing value decides. + if 'missing-value' in self.data: + if self.op(self.data['missing-value'], self.value): + matched.append(resource) + else: + # There was data and it all satisfied the condition. + matched.append(resource) + + return matched + + def process(self, resources, event=None): + # fail on an undocumented metric name even if we weren't validated + self.published_dimension_sets() + + return super().process(resources, event) + + +@SagemakerEndpoint.filter_registry.register('metrics') +class SagemakerEndpointMetricsFilter(SageMakerMetricsFilter): + """Filter sagemaker endpoints by their cloudwatch metrics. + + See the MetricsFilter doc string and + docs/source/aws/examples/sagemakermetrics.rst + """ + + VariantName = "VariantName" + ProductionVariants = "ProductionVariants" + InferenceComponentName = "InferenceComponentName" + + permissions = MetricsFilter.permissions + ( + 'sagemaker:ListInferenceComponents',) + + @functools.cached_property + def endpoint_components(self): + """Map each endpoint to the inference components hosted on it. + + An endpoint without inference components is assumed to be a classic endpoint. + """ + client = local_session( + self.manager.session_factory).client('sagemaker') + components = {} + for page in client.get_paginator( + 'list_inference_components').paginate(): + for summary in page['InferenceComponents']: + components.setdefault(summary[self.resource_dimension_name], []).append( + summary['InferenceComponentName']) + return components + + def resource_kind(self, resource) -> Kind: + """How this endpoint hosts its models. + + An endpoint built to host components but hosting none right now + is reported classic, which costs nothing: its invocations aren't + published per variant, and nothing is reserving the instance, so + the metrics that only a component endpoint publishes have no data + either. Reading the endpoint's configuration instead -- an + execution role and no variant naming a model -- would classify it + correctly at the price of a DescribeEndpointConfig per endpoint. + """ + if self.endpoint_components.get(resource[self.resource_dimension_name]): + return 'inference-component' + return 'classic' + + def can_enumerate_dimension(self, dimension_name): + return dimension_name in (self.VariantName, self.InferenceComponentName) + + def enumerate_dimension(self, dimension_name, resource) -> collections.abc.Iterable[str]: + """The values of a dimension naming this endpoint's sub units.""" + if dimension_name == self.VariantName: + return [variant[self.VariantName] + for variant in resource[self.ProductionVariants]] + + if dimension_name == self.InferenceComponentName: + return self.endpoint_components.get(resource[self.resource_dimension_name], ()) + + raise AssertionError(f"{self} can't enumerate {dimension_name}") + + @functools.cached_property + def resource_dimension_derived_value(self) -> typing.Optional[str]: + given_dimensions = self.data.get('dimensions') + if self.InferenceComponentName in given_dimensions: + inference_component_name = given_dimensions[self.InferenceComponentName] + for endpoint_name, inference_component_names in self.endpoint_components.items(): + if inference_component_name in inference_component_names: + return endpoint_name + + class EndpointConfigDescribe(DescribeSource): def augment(self, resources): diff --git a/docs/source/aws/examples/sagemakermetrics.rst b/docs/source/aws/examples/sagemakermetrics.rst new file mode 100644 index 00000000000..aea3d5d2d56 --- /dev/null +++ b/docs/source/aws/examples/sagemakermetrics.rst @@ -0,0 +1,188 @@ +Filtering SageMaker endpoints on CloudWatch metrics +=================================================== + +The ``metrics`` filter selects SageMaker endpoints by their CloudWatch +metrics: endpoints nobody is calling, GPUs barely being used, instances +that were over-provisioned for the work, and so on. + +Overview +-------- + +Metrics filters select resources according to metric values. A filter +states a condition against those values: + +.. code-block:: yaml + + value: 0 + op: lte + +A metric has one or more time series, each with zero or more values. The +filter matches a resource when the condition holds for every one of the +values. + +The values arrive one per interval: ``days`` sets the window and ``period`` +divides it into intervals, in seconds. So ``days: 14, period: 86400`` gives +fourteen daily values, and all fourteen have to satisfy the condition -- one +busy day is enough to exclude a resource. Leave ``period`` out to get a +single value for the whole window. + +Missing values for totals +------------------------- + +For metrics that count events, or sum values over events, CloudWatch +publishes nothing at all for an interval in which no event occurred. For +many analyses it is better to record non-occurrence as a zero count or sum. +Supply a ``missing-value``, generally 0: + +.. code-block:: yaml + + missing-value: 0 + +Don't do this for utilization metrics. No ``Invocations`` value means no +request arrived, but no ``CPUUtilization`` value means nothing was measured, +which is not the same as nothing being used. + +Whether a metric behaves this way is up to the service publishing it, and +endpoints don't: an endpoint reports a zero for an interval nothing called +it, so a policy looking for idle endpoints needs no missing value. See +`Endpoints that serve no traffic`_ for when one is called for. + +Filtering which metric data for a resource is considered +-------------------------------------------------------- + +Sometimes, you may not want to use all of the metric data for a +resource metric filter. To some degree, you can choose which metric +data to use. + +AWS CloudWatch defines `Dimensions +`_, +which are used to identify metric time series. Each resource metric +defines one or more dimension sets. A dimension set defines the keys +that can be used to look up individual metric time series. + +For example, SageMaker endpoints define a dimension set consisting of +Endpoint name and variant name. You can look up an individual metric +by supplying a specific endpoint name and a specific variant name. + +A metric's identity is the whole set, so only the sets CloudWatch +publishes can be asked for. The AWS documentation lists the dimensions a +metric can be filtered by rather than the sets it is published under, and +some of those combinations carry no data. + +Most resource-metrics have only one dimension set, but SageMaker +resources typically have many. + +To limit the metric data used, specify one or more dimension values:: + + dimensions: + VariantName: gpu + +For SageMaker endpoints, supplying a variant of ``gpu`` means only +the metrics identified for the GPU variant are used. + +Allowable dimensions are documented for each resource below. A +resource's own identifier (e.g. "EndpointName") is supplied for you, so +there's rarely a reason to name it; naming a different resource's leaves +nothing to measure. + +SageMaker Endpoints +------------------- + +There are two kinds of endpoints: + +- Classic endpoints that deploy models in variants + +- Inference-component endpoints that deploy models in inference + components in variants + +Which dimensions you can supply depends on the metric, and for +invocations on the kind of endpoint as well. + +Utilization metrics -- ``CPUUtilization`` and the rest -- are reported per +variant whichever kind of endpoint publishes them, so ``VariantName`` +narrows them in both cases: + +.. code-block:: yaml + + dimensions: + VariantName: gpu + +Invocations and latencies are reported per variant by a classic endpoint +and per component by a component endpoint, so the two kinds take different +dimensions: + +``VariantName`` + Narrows a classic endpoint's invocations to one variant. A component + endpoint doesn't report invocations per variant, so naming one leaves it + with nothing to measure, and unless a ``missing-value`` is supplied it + drops out of the results. + +``InferenceComponentName`` + Narrows a component endpoint's invocations to one component. Naming one + leaves every classic endpoint with nothing to measure, and the same + applies to them. + +Either way, an endpoint with nothing to measure is decided by the +``missing-value``, if the policy gives one, exactly as an endpoint whose +series reported nothing would be. + + +Endpoints that serve no traffic +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An endpoint bills for its instances from creation until it is deleted, +whether or not anything calls it. + +.. code-block:: yaml + + policies: + - name: sagemaker-endpoints-idle + resource: aws.sagemaker-endpoint + description: | + In-service endpoints with no invocations in the last week + filters: + - EndpointStatus: InService + - type: metrics + name: Invocations + statistics: Sum + days: 7 + period: 86400 + value: 0 + op: lte + +An endpoint reports a zero for an interval in which nothing called it, +whichever way it hosts its models, so its own values answer the question +and no ``missing-value`` is needed here. + +Supply one when a metric may have no values at all: for an endpoint +created part way through the window, or for a metric its kind of endpoint +doesn't publish. Then ``missing-value: 0`` reads the absence as a zero, +and leaving it out passes the endpoint over. + +Endpoints with under-used GPUs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Utilization metrics are reported per variant, like invocations: + +.. code-block:: yaml + + policies: + - name: sagemaker-endpoints-underused-gpu + resource: aws.sagemaker-endpoint + filters: + - EndpointStatus: InService + - type: metrics + name: GPUMemoryUtilization + statistics: Average + days: 14 + period: 86400 + value: 20 + op: less-than + +``CPUUtilization``, ``MemoryUtilization``, ``GPUUtilization`` and +``DiskUtilization`` are reported the same way. + +Each metric belongs to one CloudWatch namespace, which the filter looks +up from the metric name, so policies don't name it. For the metrics +SageMaker publishes, see `SageMaker metrics in CloudWatch +`_. diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..56d31f95371 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "EndpointConfigName": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "ProductionVariants": [ + { + "VariantName": "quiet", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 724000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "busy", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 779000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "gpu", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 839000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8b38596db9e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "EndpointConfigName": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 434000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_3.json new file mode 100644 index 00000000000..32e53e72535 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointConfigName": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_4.json new file mode 100644 index 00000000000..b2142ee6568 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.DescribeEndpoint_4.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointConfigName": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..f2f9d4d06b9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,108 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListInferenceComponents_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListInferenceComponents_1.json new file mode 100644 index 00000000000..320227f39a9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListInferenceComponents_1.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "InferenceComponents": [ + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 48, + "microsecond": 12000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic-idle", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 9, + "second": 3, + "microsecond": 549000 + } + }, + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 47, + "microsecond": 962000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 26, + "microsecond": 868000 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_3.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_4.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/api.sagemaker.ListTags_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..8b71f814d5c --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Sum": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..30c25997016 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Sum": 5.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..8b71f814d5c --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Sum": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_1.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_2.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_2.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_3.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_3.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_4.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_4.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_5.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_5.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle/runtime.sagemaker.InvokeEndpoint_5.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..56d31f95371 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "EndpointConfigName": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "ProductionVariants": [ + { + "VariantName": "quiet", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 724000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "busy", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 779000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "gpu", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 839000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8b38596db9e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "EndpointConfigName": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 434000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_3.json new file mode 100644 index 00000000000..32e53e72535 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointConfigName": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_4.json new file mode 100644 index 00000000000..b2142ee6568 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.DescribeEndpoint_4.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointConfigName": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..f2f9d4d06b9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,108 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListInferenceComponents_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListInferenceComponents_1.json new file mode 100644 index 00000000000..320227f39a9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListInferenceComponents_1.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "InferenceComponents": [ + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 48, + "microsecond": 12000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic-idle", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 9, + "second": 3, + "microsecond": 549000 + } + }, + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 47, + "microsecond": 962000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 26, + "microsecond": 868000 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_3.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_4.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/api.sagemaker.ListTags_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..739adaef74b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_idle_component/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 20, + "second": 0, + "microsecond": 0 + }, + "Sum": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..56d31f95371 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "EndpointConfigName": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "ProductionVariants": [ + { + "VariantName": "quiet", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 724000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "busy", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 779000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "gpu", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 839000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8b38596db9e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "EndpointConfigName": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 434000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_3.json new file mode 100644 index 00000000000..32e53e72535 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointConfigName": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_4.json new file mode 100644 index 00000000000..b2142ee6568 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.DescribeEndpoint_4.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointConfigName": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..f2f9d4d06b9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,108 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListInferenceComponents_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListInferenceComponents_1.json new file mode 100644 index 00000000000..320227f39a9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListInferenceComponents_1.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "InferenceComponents": [ + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 48, + "microsecond": 12000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic-idle", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 9, + "second": 3, + "microsecond": 549000 + } + }, + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 47, + "microsecond": 962000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 26, + "microsecond": 868000 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_3.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_4.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/api.sagemaker.ListTags_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..f8c0551d56d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 9, + "hour": 14, + "minute": 20, + "second": 0, + "microsecond": 0 + }, + "Sum": 5.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_1.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_2.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_2.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_3.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_3.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_4.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_4.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_5.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_5.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component/runtime.sagemaker.InvokeEndpoint_5.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..56d31f95371 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "EndpointConfigName": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "ProductionVariants": [ + { + "VariantName": "quiet", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 724000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "busy", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 779000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "gpu", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 839000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8b38596db9e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "EndpointConfigName": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 434000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_3.json new file mode 100644 index 00000000000..32e53e72535 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointConfigName": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_4.json new file mode 100644 index 00000000000..b2142ee6568 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.DescribeEndpoint_4.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointConfigName": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..f2f9d4d06b9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,108 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListInferenceComponents_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListInferenceComponents_1.json new file mode 100644 index 00000000000..320227f39a9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListInferenceComponents_1.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "InferenceComponents": [ + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 48, + "microsecond": 12000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic-idle", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 9, + "second": 3, + "microsecond": 549000 + } + }, + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 47, + "microsecond": 962000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 26, + "microsecond": 868000 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_3.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_4.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/api.sagemaker.ListTags_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..fd485fd6271 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_inference_component_utilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 20, + "second": 0, + "microsecond": 0 + }, + "Average": 9.132558634986877, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..56d31f95371 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,104 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "EndpointConfigName": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "ProductionVariants": [ + { + "VariantName": "quiet", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 724000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "busy", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 779000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + }, + { + "VariantName": "gpu", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 839000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8b38596db9e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "EndpointConfigName": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 434000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_3.json new file mode 100644 index 00000000000..32e53e72535 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointConfigName": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_4.json new file mode 100644 index 00000000000..b2142ee6568 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.DescribeEndpoint_4.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointConfigName": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1, + "RoutingConfig": { + "RoutingStrategy": "LEAST_OUTSTANDING_REQUESTS" + } + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..f2f9d4d06b9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,108 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 37, + "microsecond": 103000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 13, + "microsecond": 993000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 36, + "microsecond": 645000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 12, + "microsecond": 731000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 158000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 428000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 4, + "second": 27, + "microsecond": 71000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 35, + "microsecond": 273000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListInferenceComponents_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListInferenceComponents_1.json new file mode 100644 index 00000000000..320227f39a9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListInferenceComponents_1.json @@ -0,0 +1,64 @@ +{ + "status_code": 200, + "data": { + "InferenceComponents": [ + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 48, + "microsecond": 12000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic-idle", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 9, + "second": 3, + "microsecond": 549000 + } + }, + { + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 6, + "second": 47, + "microsecond": 962000 + }, + "InferenceComponentArn": "arn:aws:sagemaker:us-east-1:644160558196:inference-component/c7n-endpoint-metrics-bold-fowl-ic", + "InferenceComponentName": "c7n-endpoint-metrics-bold-fowl-ic", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "EndpointName": "c7n-endpoint-metrics-bold-fowl-ic", + "VariantName": "AllTraffic", + "InferenceComponentStatus": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 14, + "minute": 8, + "second": 26, + "microsecond": 868000 + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_3.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_3.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_4.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_4.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/api.sagemaker.ListTags_4.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..c834dffa23b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 18.746358173165525, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..3fb61187da7 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 27.125257162332026, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..1be6d86d9a6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_metrics_utilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 44.3097128979314, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..3c7768fc828 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 7.32319053277509, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..f566d1968b8 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 10.584102293341275, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..42d251c74a3 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_CPUUtilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 19.744314779775227, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..de0bae8c521 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ConcurrentRequestsPerModel", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..70a34228ce4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ConcurrentRequestsPerModel", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.019417475728155338, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..ef4988c99bc --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ConcurrentRequestsPerModel/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ConcurrentRequestsPerModel", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.009523809523809525, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..09fdab74f74 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "DiskUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 22.359322222222218, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..6bb62cb6f9d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "DiskUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 22.359272222222227, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..a115528256f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_DiskUtilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "DiskUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 5.15761842105263, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..b6a3fdf9e7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUMemoryUtilization", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..b6a3fdf9e7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUMemoryUtilization", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..2c1b331cf0f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUMemoryUtilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUMemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.018810677497901215, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..a81762f4e45 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUUtilization", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..a81762f4e45 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUUtilization", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..9cb15764dc2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_GPUUtilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..6f9a40bbd66 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation4XXErrors", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..c8ffb442e6e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation4XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..c8ffb442e6e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation4XXErrors/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation4XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..b627d45b940 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation5XXErrors", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..67087ef70b2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation5XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..67087ef70b2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocation5XXErrors/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation5XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..31abf93278b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationModelErrors", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..990389566d6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationModelErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..990389566d6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationModelErrors/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationModelErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..f7f0e29b53d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..71e94d96f6b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.4, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..6d1e869876c --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_Invocations/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.23809523809523808, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..650e7afa6b1 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsPerInstance", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..30b524b2c37 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsPerInstance", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.4, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..b01a162f2da --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_InvocationsPerInstance/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsPerInstance", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.23809523809523808, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..0e5634ccc61 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 10.507735915526146, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..5a5707babd2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 10.599132301301815, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..56baa4fd268 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_MemoryUtilization/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 3.743752571439258, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..52024957db6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "ModelLatency", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..1fc8e3065fa --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ModelLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 11337.9, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..8a05e85c9f9 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_ModelLatency/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ModelLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 4921.6, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..0496f06edef --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "OverheadLatency", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..89b6aab29c7 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "OverheadLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 65526.1, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_3.json b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_3.json new file mode 100644 index 00000000000..10f53c43f49 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_classic_OverheadLatency/monitoring.GetMetricStatistics_3.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "OverheadLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 18381.0, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..acb4cc1d6c1 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUReservation", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 25.0, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_1.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_1.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_10.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_10.json new file mode 100644 index 00000000000..ea6ec6a6f7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_10.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "gpu", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_11.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_11.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_11.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_12.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_12.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_12.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_13.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_13.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_13.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_14.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_14.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_14.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_15.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_15.json new file mode 100644 index 00000000000..f2262b1d3e4 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_15.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "AllTraffic", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_2.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_2.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_2.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_3.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_3.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_3.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_4.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_4.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_4.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_5.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_5.json new file mode 100644 index 00000000000..be37ecd02e6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_5.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "busy", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_6.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_6.json new file mode 100644 index 00000000000..ea6ec6a6f7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_6.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "gpu", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_7.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_7.json new file mode 100644 index 00000000000..ea6ec6a6f7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_7.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "gpu", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_8.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_8.json new file mode 100644 index 00000000000..ea6ec6a6f7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_8.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "gpu", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_9.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_9.json new file mode 100644 index 00000000000..ea6ec6a6f7d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUReservation/runtime.sagemaker.InvokeEndpoint_9.json @@ -0,0 +1,13 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ContentType": "text/csv; charset=utf-8", + "InvokedProductionVariant": "gpu", + "Body": { + "__class__": "StreamingBody", + "__module__": "botocore.response", + "body": "MC41NzQ5OTk5ODgwNzkwNzEK" + } + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..8b9f2c5f3ab --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 6.472713090727191, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..810f5b896f8 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_CPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilizationNormalized", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 1.6181782726817977, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_ConcurrentRequestsPerCopy/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_ConcurrentRequestsPerCopy/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..771d338b1f2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_ConcurrentRequestsPerCopy/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ConcurrentRequestsPerCopy", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0196078431372549, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_DiskUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_DiskUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..8bbf18611b2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_DiskUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "DiskUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 5.157698235294119, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..8f5aae93f0d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUMemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.01868771764705882, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilizationNormalized/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilizationNormalized/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..369955a173f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUMemoryUtilizationNormalized/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUMemoryUtilizationNormalized", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 0.01868771764705882, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_GPUReservation/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUReservation/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..bc9a5580e74 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUReservation/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUReservation", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 100.0, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..9cb15764dc2 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..806e4e36c6e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_GPUUtilizationNormalized/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "GPUUtilizationNormalized", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..0a244a97056 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation4XXErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation4XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..b3796381f39 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocation5XXErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocation5XXErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationModelErrors/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationModelErrors/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..990389566d6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationModelErrors/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationModelErrors", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_Invocations/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocations/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..71e94d96f6b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_Invocations/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "Invocations", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.4, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationsPerCopy/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationsPerCopy/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..3246b587537 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_InvocationsPerCopy/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsPerCopy", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 0.4, + "Unit": "None" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryReservation/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryReservation/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..54b8b765c5f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryReservation/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryReservation", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 25, + "second": 0, + "microsecond": 0 + }, + "Average": 7.660083782166367, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..3c2e4b1d4e8 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_MemoryUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 3.675538957167248, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_ModelLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_ModelLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..604438fa863 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_ModelLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ModelLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 4249.3, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_inference_component_OverheadLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_inference_component_OverheadLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..46e3d567d70 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_inference_component_OverheadLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "OverheadLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 9, + "day": 10, + "hour": 13, + "minute": 26, + "second": 0, + "microsecond": 0 + }, + "Average": 16127.7, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/sagemaker_endpoint_metrics/README.md b/tests/terraform/sagemaker_endpoint_metrics/README.md new file mode 100644 index 00000000000..0bacd10ece3 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_metrics/README.md @@ -0,0 +1,48 @@ +# sagemaker_endpoint_metrics + +Four endpoints sharing one model and one execution role, covering the two +ways SageMaker reports endpoint metrics, each with one that serves traffic +and one that serves none. + +`busy` and `idle` are classic endpoints: the model is attached to each +production variant, and metrics are dimensioned by `EndpointName, +VariantName`. `busy` has three variants -- `quiet`, `busy` and `gpu` -- and +everything but `quiet` is invoked, so the recorded data covers variants with +invocations, a variant without, and (in `idle`) an endpoint with none. A +filter that queried only the first variant would report `busy` as idle. +`gpu` is an `ml.g4dn.xlarge`, which is what makes the GPU metrics report; +nothing here uses the GPU, but the instance having one is enough. + +`ic` and `ic-idle` are inference-component based: their configurations carry +an execution role and their variants name no model, so each variant is a +compute pool and the model arrives as an inference component. Such an +endpoint publishes its invocations under `InferenceComponentName` alone, +with no `EndpointName` dimension, while its utilization metrics stay +dimensioned by `EndpointName, VariantName`. `ic` is invoked and `ic-idle` +never is, which is how we know an uncalled component endpoint reports zeros +rather than nothing at all. + +An endpoint hosting components takes only one variant and its instance type +can't be changed after creation, so `ic` is a GPU pool with one component +reserving the accelerator, and `ic-idle` a cheaper CPU pool. + +Neither the AWS provider nor OpenTofu has an inference component resource, +so `aws_cloudformation_stack` stands in for one. Destroying the stack +destroys the component. + +`probe_metrics.py` checks `c7n/data/sagemaker_metrics.yaml` against what +these endpoints publish, and documents how to update it. Run it with +credentials for this account once the endpoints are `InService`. + +`model.tar.gz` is the smallest artifact the prebuilt XGBoost serving image +will load: + +```bash +uv run --no-project --with xgboost python -c " +import xgboost, numpy +d = xgboost.DMatrix(numpy.array([[0.0], [1.0]]), label=numpy.array([0.0, 1.0])) +booster = xgboost.train({'objective': 'reg:squarederror'}, d, num_boost_round=1) +booster.save_model('xgboost-model.json')" +mv xgboost-model.json xgboost-model +tar czf model.tar.gz xgboost-model +``` diff --git a/tests/terraform/sagemaker_endpoint_metrics/main.tf b/tests/terraform/sagemaker_endpoint_metrics/main.tf new file mode 100644 index 00000000000..d9c6e00c096 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_metrics/main.tf @@ -0,0 +1,300 @@ +provider "aws" {} + +resource "random_pet" "main" { + length = 2 + separator = "-" +} + +locals { + name = "c7n-endpoint-metrics-${random_pet.main.id}" + + # the stacks below name the components, and the outputs report those + # names to the tests, so both read them from here + component = "${local.name}-ic" + idle_component = "${local.name}-ic-idle" +} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["sagemaker.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "execution" { + name = local.name + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_iam_role_policy_attachment" "execution" { + role = aws_iam_role.execution.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess" +} + +# the bucket name has to contain "sagemaker" for AmazonSageMakerFullAccess to +# grant the execution role access to it +resource "aws_s3_bucket" "model" { + bucket = "c7n-sagemaker-endpoint-metrics-${random_pet.main.id}" + force_destroy = true +} + +# see README.md for how model.tar.gz is built +resource "aws_s3_object" "model" { + bucket = aws_s3_bucket.model.id + key = "model.tar.gz" + source = "${path.module}/model.tar.gz" + etag = filemd5("${path.module}/model.tar.gz") +} + +data "aws_sagemaker_prebuilt_ecr_image" "xgboost" { + repository_name = "sagemaker-xgboost" + image_tag = "1.7-1" +} + +resource "aws_sagemaker_model" "main" { + name = local.name + execution_role_arn = aws_iam_role.execution.arn + + # the arn alone orders this after the role, not after its policy, and an + # endpoint whose role can't yet read the artifact fails to come up + depends_on = [aws_iam_role_policy_attachment.execution] + + primary_container { + image = data.aws_sagemaker_prebuilt_ecr_image.xgboost.registry_path + model_data_url = "s3://${aws_s3_bucket.model.id}/${aws_s3_object.model.key}" + } +} + +## +## Classic endpoints: the model is attached to each production variant. +## + +# Every variant but "quiet" is invoked, so an idle-endpoint policy must skip +# this endpoint -- which it can only do by querying more than its first +# variant. "gpu" is here so the GPU metrics have an instance to report. +# A configuration can't be edited, and an endpoint follows its configuration +# by name, so the name has to change for a variant change to reach the +# endpoint -- hence name_prefix, which terraform makes unique. The prefix is +# short because AWS caps it at 37 characters, well under local.name. +resource "aws_sagemaker_endpoint_configuration" "busy" { + name_prefix = "c7n-em-busy-" + + lifecycle { + create_before_destroy = true + } + + production_variants { + variant_name = "quiet" + model_name = aws_sagemaker_model.main.name + initial_instance_count = 1 + instance_type = "ml.c5.large" + initial_variant_weight = 1 + } + + production_variants { + variant_name = "busy" + model_name = aws_sagemaker_model.main.name + initial_instance_count = 1 + instance_type = "ml.c5.large" + initial_variant_weight = 1 + } + + # a gpu variant so the endpoint publishes the GPU metrics; nothing here + # uses the gpu, but the instance having one is what makes them appear + production_variants { + variant_name = "gpu" + model_name = aws_sagemaker_model.main.name + initial_instance_count = 1 + instance_type = "ml.g4dn.xlarge" + initial_variant_weight = 1 + } +} + +resource "aws_sagemaker_endpoint_configuration" "idle" { + name_prefix = "c7n-em-idle-" + + lifecycle { + create_before_destroy = true + } + + production_variants { + variant_name = "AllTraffic" + model_name = aws_sagemaker_model.main.name + initial_instance_count = 1 + instance_type = "ml.c5.large" + initial_variant_weight = 1 + } +} + +resource "aws_sagemaker_endpoint" "busy" { + name = "${local.name}-busy" + endpoint_config_name = aws_sagemaker_endpoint_configuration.busy.name +} + +resource "aws_sagemaker_endpoint" "idle" { + name = "${local.name}-idle" + endpoint_config_name = aws_sagemaker_endpoint_configuration.idle.name +} + +## +## Inference-component endpoint: the configuration carries an execution role +## and no variant names a model, so the variant is a compute pool and the +## model arrives as a component placed on it. Such an endpoint reports its +## invocations per component, with no EndpointName dimension at all. +## + +resource "aws_sagemaker_endpoint_configuration" "component" { + name_prefix = "c7n-em-ic-" + execution_role_arn = aws_iam_role.execution.arn + + lifecycle { + create_before_destroy = true + } + + # an endpoint with inference components can have only one variant, so the + # pool has a gpu and both components share it -- one reserving the + # accelerator, one not + production_variants { + variant_name = "AllTraffic" + instance_type = "ml.g4dn.xlarge" + initial_instance_count = 1 + + routing_config { + routing_strategy = "LEAST_OUTSTANDING_REQUESTS" + } + } +} + +# An endpoint hosting inference components can't be updated to a different +# instance type, so a pool change means replacing the endpoint -- and its +# components with it, hence the stack below follows. +resource "aws_sagemaker_endpoint" "component" { + name = "${local.name}-ic" + endpoint_config_name = aws_sagemaker_endpoint_configuration.component.name + + lifecycle { + replace_triggered_by = [aws_sagemaker_endpoint_configuration.component] + } +} + +# Neither the aws provider nor OpenTofu has an inference component resource, +# but cloudformation does, so the stack stands in for one -- destroying the +# stack destroys the component. +resource "aws_cloudformation_stack" "component" { + name = "${local.name}-ic" + + lifecycle { + replace_triggered_by = [aws_sagemaker_endpoint.component] + } + + template_body = jsonencode({ + Resources = { + Component = { + Type = "AWS::SageMaker::InferenceComponent" + Properties = { + InferenceComponentName = local.component + EndpointName = aws_sagemaker_endpoint.component.name + VariantName = "AllTraffic" + Specification = { + ModelName = aws_sagemaker_model.main.name + # every component on a gpu pool has to reserve accelerators, + # and ml.g4dn.xlarge has one, so the endpoint hosts one component + ComputeResourceRequirements = { + MinMemoryRequiredInMb = 1024 + NumberOfCpuCoresRequired = 1 + NumberOfAcceleratorDevicesRequired = 1 + } + } + RuntimeConfig = { CopyCount = 1 } + } + } + } + }) +} + +## +## A component endpoint nothing ever calls. A classic endpoint publishes a +## zero for every interval whether or not it is invoked, but this one +## publishes no invocation data at all, so only a missing-value can decide +## it. Its pool has no gpu, so its component reserves no accelerator. +## + +resource "aws_sagemaker_endpoint_configuration" "component_idle" { + name_prefix = "c7n-em-ic-idle-" + execution_role_arn = aws_iam_role.execution.arn + + lifecycle { + create_before_destroy = true + } + + production_variants { + variant_name = "AllTraffic" + instance_type = "ml.c5.large" + initial_instance_count = 1 + + routing_config { + routing_strategy = "LEAST_OUTSTANDING_REQUESTS" + } + } +} + +resource "aws_sagemaker_endpoint" "component_idle" { + name = "${local.name}-ic-idle" + endpoint_config_name = aws_sagemaker_endpoint_configuration.component_idle.name + + lifecycle { + replace_triggered_by = [aws_sagemaker_endpoint_configuration.component_idle] + } +} + +resource "aws_cloudformation_stack" "component_idle" { + name = "${local.name}-ic-idle" + + lifecycle { + replace_triggered_by = [aws_sagemaker_endpoint.component_idle] + } + + template_body = jsonencode({ + Resources = { + Component = { + Type = "AWS::SageMaker::InferenceComponent" + Properties = { + InferenceComponentName = local.idle_component + EndpointName = aws_sagemaker_endpoint.component_idle.name + VariantName = "AllTraffic" + Specification = { + ModelName = aws_sagemaker_model.main.name + ComputeResourceRequirements = { + MinMemoryRequiredInMb = 1024 + NumberOfCpuCoresRequired = 1 + } + } + RuntimeConfig = { CopyCount = 1 } + } + } + } + }) +} + +output "idle_component_endpoint_name" { + value = aws_sagemaker_endpoint.component_idle.name +} + +# created by the stack, so depend on it rather than on the name alone +output "idle_component_name" { + value = local.idle_component + depends_on = [aws_cloudformation_stack.component_idle] +} + +output "component_endpoint_name" { + value = aws_sagemaker_endpoint.component.name +} + +# created by the stack, so depend on it rather than on the name alone +output "component_name" { + value = local.component + depends_on = [aws_cloudformation_stack.component] +} diff --git a/tests/terraform/sagemaker_endpoint_metrics/model.tar.gz b/tests/terraform/sagemaker_endpoint_metrics/model.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6ccfd873df8cf521c4612b136818d3763ece13da GIT binary patch literal 587 zcmV-R0<`@fiwFP!000001MQSyZ<{a>hVxS*es6%){x_XRvO9lkWx@YV&Wl&M}Q#-p24 zZ~!XSQD^ORM^*Zvi$U9BmNtlkkRpkB3Vn?6b%Q!|Ft%5wug(EA9n(bOL9wdoYfG6a zCJk3{(jL@TFwIjGwMie+WK(?FJ#4aMeBgGx4m8%8`R;GA>QU;i+52lQ7tHs0dj+)~ z+MrsfLllaCm^PI-q=}%cDxXYSIp}d&CL8+44XyPaS9IURN*Rfi3F*cUu4AXBqcfQo z`RDCEFQ(d#uDPt5y;k+-*5H!uD?<&W*X8e8G48|mnlc_g%O3OOk7W`E^aS1;9S_5L za{SCAxf?Og9MBFp%yb&=nD3o*Ta&i$>WT$rQ0M&f8YM~&k4gY48t%C!!QiPFbu;m Z48t%C!!QiPFbwmT`2h~am$d*W003_nBzyn> literal 0 HcmV?d00001 diff --git a/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py b/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py new file mode 100644 index 00000000000..6df3c7ec9d2 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py @@ -0,0 +1,321 @@ +"""Check sagemaker_metrics.yaml against what these endpoints really publish. + +Reads the file as written, section by section, because its job is to +help write that file. What the filters make of it is their own affair, +and the test suite covers it. + +NOTE! This is weird :smile:. Part documentation, part prompt, part +code, and it will need to be updated to add more resources. It is +what it is, but it's still valuable. + +Run against the endpoints this terraform module creates, with credentials +for the account they live in: + + terraform apply + AWS_DEFAULT_REGION=us-east-1 uv run tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py + +Everything goes to standard output. Use --no-invoke to skip generating +traffic when the endpoints have been invoked within the metric window. + +--- Updating c7n/data/sagemaker_metrics.yaml --- + +For an agent asked to bring the catalogue up to date. Two sources, and +they answer different questions. + +The documentation is the only source of metric *names* and namespaces: + + https://docs.aws.amazon.com/sagemaker/latest/dg/monitoring-cloudwatch.html + +Fetch it as markdown -- the same URL with .md instead of .html serves the +tables without the page chrome. Each metric table becomes a section whose +`title` is the table's caption; the namespace is stated in the prose above +the table, not in it. Add metrics the page has gained, remove ones it has +dropped, and keep each section's metric list in the page's order so the two +can be read side by side. + +This script is the only source of *dimension sets* and of `kind`. +Do not take the page's dimensions tables literally: they list the dimensions +a metric can be filtered by, and CloudWatch identifies a series by an exact +set of dimensions. Instance type, for example, is documented as +"EndpointName, VariantName, InstanceType" but is only ever published +alongside AvailabilityZone and Region, so a query on the documented three +returns nothing. + +So, for dimensions: + + - List only sets this script reports as carrying data. + - Never add a set because the page lists it. + - Never add a set this script found but the page doesn't mention: those + exist, but c7n has no way to supply an AvailabilityZone or a Region. + +And for `kind`, `classic` means a model is attached to each +production variant, `inference-component` means the configuration carries an +execution role and models arrive as components. A section's metrics belong +to the kinds this script saw publishing them. + +The script cannot see everything. It reports which catalogue entries it +could not check -- metrics no endpoint here publishes, such as the GPU +metrics with no GPU instance in the account, and the multi-model metrics +with no multi-model endpoint. Those entries stay as they are, and their +`kind` remains reasoning by analogy with the metrics that were checked. +Say so in a comment rather than implying they were measured. + +""" + +import argparse +import collections +import datetime +import pathlib +import time + +import boto3 +import yaml + +CATALOGUE = (pathlib.Path(__file__).parents[3] + / 'c7n' / 'data' / 'sagemaker_metrics.yaml') + +PREFIX = 'c7n-endpoint-metrics-' + +NAMESPACES = ( + 'AWS/SageMaker', + '/aws/sagemaker/Endpoints', + '/aws/sagemaker/InferenceComponents', + ) + + +def endpoint_kind(sagemaker, endpoint: dict) -> str: + """Which way this endpoint hosts its models. + + The rule the SageMaker SDK uses: an execution role on the + configuration and no production variant naming a model. + """ + config = sagemaker.describe_endpoint_config( + EndpointConfigName=endpoint['EndpointConfigName']) + if config.get('ExecutionRoleArn') and not any( + 'ModelName' in variant for variant in config['ProductionVariants']): + return 'inference-component' + return 'classic' + + +def find_endpoints(sagemaker) -> list[dict]: + endpoints = [] + for page in sagemaker.get_paginator('list_endpoints').paginate(): + for summary in page['Endpoints']: + if summary['EndpointName'].startswith(PREFIX): + endpoint = sagemaker.describe_endpoint( + EndpointName=summary['EndpointName']) + endpoint['kind'] = endpoint_kind(sagemaker, endpoint) + endpoints.append(endpoint) + return endpoints + + +def find_components(sagemaker, endpoints: list[dict]) -> dict[str, list[str]]: + """The components on our endpoints, ignoring the rest of the account.""" + ours = {endpoint['EndpointName'] for endpoint in endpoints} + components = collections.defaultdict(list) + for page in sagemaker.get_paginator( + 'list_inference_components').paginate(): + for summary in page['InferenceComponents']: + if summary['EndpointName'] in ours: + components[summary['EndpointName']].append( + summary['InferenceComponentName']) + return components + + +def invoke(runtime, endpoints, components, count: int) -> None: + """Invoke one variant of each classic endpoint, and each component. + + A variant named "quiet" is left alone: a variant that is never invoked + still publishes zeros, which is worth seeing. + """ + for endpoint in endpoints: + name = endpoint['EndpointName'] + targets = [ + dict(InferenceComponentName=component) + for component in components.get(name, ()) + ] or [ + dict(TargetVariant=variant['VariantName']) + for variant in endpoint['ProductionVariants'] + if variant['VariantName'] != 'quiet' + ] + for target in targets: + for _ in range(count): + runtime.invoke_endpoint( + EndpointName=name, ContentType='text/csv', + Body=b'0.5\n', **target) + print(f" invoked {name} {count}x {target}") + + +def observed(cloudwatch, names: set[str]) -> dict: + """What CloudWatch reports for our resources. + + {(namespace, resource name): {metric name: {dimension name set}}}, + where the resource name is the endpoint or component named in the + metric's dimensions. + """ + seen: dict = collections.defaultdict( + lambda: collections.defaultdict(set)) + for namespace in NAMESPACES: + for page in cloudwatch.get_paginator('list_metrics').paginate( + Namespace=namespace): + for metric in page['Metrics']: + name_set = tuple(sorted(d['Name'] for d in metric['Dimensions'])) + for dimension in metric['Dimensions']: + if dimension['Value'] in names: + seen[namespace, dimension['Value']][ + metric['MetricName']].add(name_set) + return seen + + +def fill(name_set: list[str], endpoint: dict, component: str | None) -> list[list[dict]]: + """The dimensions to query for one resource, or [] if we can't supply them.""" + if name_set == ['EndpointName', 'VariantName']: + return [ + [{'Name': 'EndpointName', 'Value': endpoint['EndpointName']}, + {'Name': 'VariantName', 'Value': variant['VariantName']}] + for variant in endpoint['ProductionVariants'] + ] + if name_set == ['InferenceComponentName'] and component: + return [[{'Name': 'InferenceComponentName', 'Value': component}]] + return [] + + +def has_points(cloudwatch, namespace: str, metric: str, dimensions, + window) -> bool: + start, end = window + return bool(cloudwatch.get_metric_statistics( + Namespace=namespace, MetricName=metric, StartTime=start, EndTime=end, + Period=300, Statistics=['Sum'], Dimensions=dimensions)['Datapoints']) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--no-invoke', action='store_true', + help="don't generate traffic first") + parser.add_argument('--count', type=int, default=30, + help='invocations per target (default 30)') + parser.add_argument('--wait', type=int, default=300, + help='seconds to wait for publication (default 300)') + parser.add_argument('--minutes', type=int, default=60, + help='metric window in minutes (default 60)') + args = parser.parse_args() + + sagemaker = boto3.client('sagemaker') + cloudwatch = boto3.client('cloudwatch') + + endpoints = find_endpoints(sagemaker) + if not endpoints: + raise SystemExit(f"no endpoints named {PREFIX}* -- run terraform apply") + components = find_components(sagemaker, endpoints) + + print('Endpoints') + for endpoint in endpoints: + variants = ', '.join(v['VariantName'] + for v in endpoint['ProductionVariants']) + print(f" {endpoint['EndpointName']} {endpoint['kind']}" + f" variants: {variants}" + f" components: {components.get(endpoint['EndpointName'], [])}") + + if not args.no_invoke: + print('\nInvoking') + invoke(boto3.client('sagemaker-runtime'), endpoints, components, + args.count) + print(f" waiting {args.wait}s for publication") + time.sleep(args.wait) + + end = datetime.datetime.now(datetime.timezone.utc) + window = (end - datetime.timedelta(minutes=args.minutes), end) + names = {e['EndpointName'] for e in endpoints} + names.update(c for cs in components.values() for c in cs) + + print('\nDimension sets CloudWatch reports for these resources') + seen = observed(cloudwatch, names) + by_namespace: dict = collections.defaultdict( + lambda: collections.defaultdict(set)) + for (namespace, resource), metrics in seen.items(): + for metric, name_sets in metrics.items(): + for name_set in name_sets: + by_namespace[namespace][name_set].add(metric) + for namespace in NAMESPACES: + print(f" {namespace}") + for name_set, metrics in sorted(by_namespace[namespace].items()): + print(f" {list(name_set)}: {len(metrics)} metrics") + + print('\nCatalogue entries') + unexercised: list[str] = [] + mismatched: list[str] = [] + for section in yaml.safe_load(CATALOGUE.read_text()): + namespace = section['namespace'] + name_sets = [tuple(sorted(name.strip() for name in dimensions.split(','))) + for dimensions in section['dimensions']] + # a section with no kind claims every kind publishes its metrics, + # so check them one kind at a time rather than together + section_kind = section.get('kind') + kinds = ([section_kind] if section_kind + else sorted({endpoint['kind'] for endpoint in endpoints})) + + for metric in section['metrics']: + for kind in kinds: + where = section_kind or f"any kind (as {kind})" + targets = [ + (endpoint, component) + for endpoint in endpoints if endpoint['kind'] == kind + for component in (components.get(endpoint['EndpointName']) + or [None]) + ] + published = { + name_set + for endpoint, component in targets + for named in (endpoint['EndpointName'], component) + if named + for name_set in seen[namespace, named].get(metric, ()) + } + if not published: + unexercised.append(f"{metric:32} {where}") + continue + + matching = [s for s in name_sets if s in published] + if not matching: + mismatched.append( + f"{metric:32} {where:22} file" + f" {[list(s) for s in name_sets]}," + f" published {[list(s) for s in sorted(published)]}") + continue + + queried = [ + has_points(cloudwatch, namespace, metric, filled, window) + for name_set in matching + for endpoint, component in targets + for filled in fill(list(name_set), endpoint, component) + ] + verdict = 'ok ' if any(queried) else 'no points' + print(f" {verdict} {metric:32} {where:22}" + f" {[list(s) for s in matching]}") + + if mismatched: + print('\nWrong dimensions -- the catalogue names a set that is not' + ' published') + for line in mismatched: + print(f" {line}") + + if unexercised: + print('\nNot exercised by these endpoints -- left as they are,' + ' not measured') + for line in unexercised: + print(f" {line}") + + print('\nPublished for our resources but not in the catalogue' + ' -- do not add these') + catalogued = { + tuple(sorted(name.strip() for name in dimensions.split(','))) + for section in yaml.safe_load(CATALOGUE.read_text()) + for dimensions in section['dimensions'] + } + for namespace in NAMESPACES: + for name_set in sorted(by_namespace[namespace]): + if name_set not in catalogued: + print(f" {namespace} {list(name_set)}") + + +if __name__ == '__main__': + main() diff --git a/tests/terraform/sagemaker_endpoint_metrics/tf_resources.json b/tests/terraform/sagemaker_endpoint_metrics/tf_resources.json new file mode 100644 index 00000000000..c015224288d --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_metrics/tf_resources.json @@ -0,0 +1,514 @@ +{ + "pytest-terraform": 1, + "outputs": { + "component_endpoint_name": { + "value": "c7n-endpoint-metrics-bold-fowl-ic", + "type": "string" + }, + "component_name": { + "value": "c7n-endpoint-metrics-bold-fowl-ic", + "type": "string" + }, + "idle_component_endpoint_name": { + "value": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "type": "string" + }, + "idle_component_name": { + "value": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "type": "string" + } + }, + "resources": { + "aws_iam_policy_document": { + "assume_role": { + "id": "1147345228", + "json": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Action\": \"sts:AssumeRole\",\n \"Principal\": {\n \"Service\": \"sagemaker.amazonaws.com\"\n }\n }\n ]\n}", + "minified_json": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}]}", + "override_json": null, + "override_policy_documents": null, + "policy_id": null, + "source_json": null, + "source_policy_documents": null, + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "condition": [], + "effect": "Allow", + "not_actions": [], + "not_principals": [], + "not_resources": [], + "principals": [ + { + "identifiers": [ + "sagemaker.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [], + "sid": "" + } + ], + "version": "2012-10-17" + } + }, + "aws_sagemaker_prebuilt_ecr_image": { + "xgboost": { + "dns_suffix": null, + "id": "644160558196", + "image_tag": "1.7-1", + "region": "us-east-1", + "registry_id": "644160558196", + "registry_path": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "repository_name": "sagemaker-xgboost" + } + }, + "aws_cloudformation_stack": { + "component": { + "capabilities": null, + "disable_rollback": false, + "iam_role_arn": "", + "id": "arn:aws:cloudformation:us-east-1:644160558196:stack/c7n-endpoint-metrics-bold-fowl-ic/25063290-ad53-11f1-bfe6-0edd12258b05", + "name": "c7n-endpoint-metrics-bold-fowl-ic", + "notification_arns": null, + "on_failure": null, + "outputs": {}, + "parameters": {}, + "policy_body": null, + "policy_url": null, + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "template_body": "{\"Resources\":{\"Component\":{\"Properties\":{\"EndpointName\":\"c7n-endpoint-metrics-bold-fowl-ic\",\"InferenceComponentName\":\"c7n-endpoint-metrics-bold-fowl-ic\",\"RuntimeConfig\":{\"CopyCount\":1},\"Specification\":{\"ComputeResourceRequirements\":{\"MinMemoryRequiredInMb\":1024,\"NumberOfAcceleratorDevicesRequired\":1,\"NumberOfCpuCoresRequired\":1},\"ModelName\":\"c7n-endpoint-metrics-bold-fowl\"},\"VariantName\":\"AllTraffic\"},\"Type\":\"AWS::SageMaker::InferenceComponent\"}}}", + "template_url": null, + "timeout_in_minutes": 0, + "timeouts": null + }, + "component_idle": { + "capabilities": null, + "disable_rollback": false, + "iam_role_arn": "", + "id": "arn:aws:cloudformation:us-east-1:644160558196:stack/c7n-endpoint-metrics-bold-fowl-ic-idle/24ebccc0-ad53-11f1-9198-12c0b6cd8fe5", + "name": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "notification_arns": null, + "on_failure": null, + "outputs": {}, + "parameters": {}, + "policy_body": null, + "policy_url": null, + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "template_body": "{\"Resources\":{\"Component\":{\"Properties\":{\"EndpointName\":\"c7n-endpoint-metrics-bold-fowl-ic-idle\",\"InferenceComponentName\":\"c7n-endpoint-metrics-bold-fowl-ic-idle\",\"RuntimeConfig\":{\"CopyCount\":1},\"Specification\":{\"ComputeResourceRequirements\":{\"MinMemoryRequiredInMb\":1024,\"NumberOfCpuCoresRequired\":1},\"ModelName\":\"c7n-endpoint-metrics-bold-fowl\"},\"VariantName\":\"AllTraffic\"},\"Type\":\"AWS::SageMaker::InferenceComponent\"}}}", + "template_url": null, + "timeout_in_minutes": 0, + "timeouts": null + } + }, + "aws_iam_role": { + "execution": { + "arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-bold-fowl", + "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}],\"Version\":\"2012-10-17\"}", + "create_date": "2026-09-10T20:04:25Z", + "description": "", + "force_detach_policies": false, + "id": "c7n-endpoint-metrics-bold-fowl", + "inline_policy": [], + "managed_policy_arns": [], + "max_session_duration": 3600, + "name": "c7n-endpoint-metrics-bold-fowl", + "name_prefix": "", + "path": "/", + "permissions_boundary": "", + "tags": null, + "tags_all": {}, + "unique_id": "AROAV7EQQKWX5XB5BD7UU" + } + }, + "aws_iam_role_policy_attachment": { + "execution": { + "id": "c7n-endpoint-metrics-bold-fowl/arn:aws:iam::aws:policy/AmazonSageMakerFullAccess", + "policy_arn": "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess", + "role": "c7n-endpoint-metrics-bold-fowl" + } + }, + "aws_s3_bucket": { + "model": { + "acceleration_status": "", + "acl": null, + "arn": "arn:aws:s3:::c7n-sagemaker-endpoint-metrics-bold-fowl", + "bucket": "c7n-sagemaker-endpoint-metrics-bold-fowl", + "bucket_domain_name": "c7n-sagemaker-endpoint-metrics-bold-fowl.s3.amazonaws.com", + "bucket_namespace": "global", + "bucket_prefix": "", + "bucket_region": "us-east-1", + "bucket_regional_domain_name": "c7n-sagemaker-endpoint-metrics-bold-fowl.s3.us-east-1.amazonaws.com", + "cors_rule": [], + "force_destroy": true, + "grant": [ + { + "id": "46af83c9c3bd517b24c5e1f559f1a1e4694a3a119004c8db795b301befae00ca", + "permissions": [ + "FULL_CONTROL" + ], + "type": "CanonicalUser", + "uri": "" + } + ], + "hosted_zone_id": "Z3AQBSTGFYJSTF", + "id": "c7n-sagemaker-endpoint-metrics-bold-fowl", + "lifecycle_rule": [], + "logging": [], + "object_lock_configuration": [], + "object_lock_enabled": false, + "policy": "", + "region": "us-east-1", + "replication_configuration": [], + "request_payer": "BucketOwner", + "server_side_encryption_configuration": [ + { + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "kms_master_key_id": "", + "sse_algorithm": "AES256" + } + ], + "bucket_key_enabled": false + } + ] + } + ], + "tags": null, + "tags_all": {}, + "timeouts": null, + "versioning": [ + { + "enabled": false, + "mfa_delete": false + } + ], + "website": [], + "website_domain": null, + "website_endpoint": null + } + }, + "aws_s3_object": { + "model": { + "acl": null, + "arn": "arn:aws:s3:::c7n-sagemaker-endpoint-metrics-bold-fowl/model.tar.gz", + "bucket": "c7n-sagemaker-endpoint-metrics-bold-fowl", + "bucket_key_enabled": false, + "cache_control": "", + "checksum_algorithm": null, + "checksum_crc32": "", + "checksum_crc32c": "", + "checksum_crc64nvme": "", + "checksum_sha1": "", + "checksum_sha256": "", + "content": null, + "content_base64": null, + "content_disposition": "", + "content_encoding": "", + "content_language": "", + "content_type": "application/octet-stream", + "etag": "dd8ee194115d990ad16e3ddaf17bf982", + "force_destroy": false, + "id": "c7n-sagemaker-endpoint-metrics-bold-fowl/model.tar.gz", + "key": "model.tar.gz", + "kms_key_id": null, + "metadata": null, + "object_lock_legal_hold_status": "", + "object_lock_mode": "", + "object_lock_retain_until_date": "", + "override_provider": [], + "region": "us-east-1", + "server_side_encryption": "AES256", + "source": "./model.tar.gz", + "source_hash": null, + "storage_class": "STANDARD", + "tags": null, + "tags_all": {}, + "version_id": "", + "website_redirect": "" + } + }, + "aws_sagemaker_endpoint": { + "busy": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-busy", + "deployment_config": [], + "endpoint_config_name": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "id": "c7n-endpoint-metrics-bold-fowl-busy", + "name": "c7n-endpoint-metrics-bold-fowl-busy", + "region": "us-east-1", + "tags": null, + "tags_all": {} + }, + "component": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic", + "deployment_config": [], + "endpoint_config_name": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "id": "c7n-endpoint-metrics-bold-fowl-ic", + "name": "c7n-endpoint-metrics-bold-fowl-ic", + "region": "us-east-1", + "tags": null, + "tags_all": {} + }, + "component_idle": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-ic-idle", + "deployment_config": [], + "endpoint_config_name": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "id": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "name": "c7n-endpoint-metrics-bold-fowl-ic-idle", + "region": "us-east-1", + "tags": null, + "tags_all": {} + }, + "idle": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-bold-fowl-idle", + "deployment_config": [], + "endpoint_config_name": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "id": "c7n-endpoint-metrics-bold-fowl-idle", + "name": "c7n-endpoint-metrics-bold-fowl-idle", + "region": "us-east-1", + "tags": null, + "tags_all": {} + } + }, + "aws_sagemaker_endpoint_configuration": { + "busy": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint-config/c7n-em-busy-6ae3873dc78e414286bf55b763", + "async_inference_config": [], + "data_capture_config": [], + "execution_role_arn": "", + "id": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "kms_key_arn": "", + "name": "c7n-em-busy-6ae3873dc78e414286bf55b763", + "name_prefix": "c7n-em-busy-", + "production_variants": [ + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 1, + "instance_type": "ml.c5.large", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "c7n-endpoint-metrics-bold-fowl", + "routing_config": [], + "serverless_config": [], + "variant_name": "quiet", + "volume_size_in_gb": 0 + }, + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 1, + "instance_type": "ml.c5.large", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "c7n-endpoint-metrics-bold-fowl", + "routing_config": [], + "serverless_config": [], + "variant_name": "busy", + "volume_size_in_gb": 0 + }, + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 1, + "instance_type": "ml.g4dn.xlarge", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "c7n-endpoint-metrics-bold-fowl", + "routing_config": [], + "serverless_config": [], + "variant_name": "gpu", + "volume_size_in_gb": 0 + } + ], + "region": "us-east-1", + "shadow_production_variants": [], + "tags": null, + "tags_all": {} + }, + "component": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint-config/c7n-em-ic-2fe668b931062054b3fed2e0fe", + "async_inference_config": [], + "data_capture_config": [], + "execution_role_arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-bold-fowl", + "id": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "kms_key_arn": "", + "name": "c7n-em-ic-2fe668b931062054b3fed2e0fe", + "name_prefix": "c7n-em-ic-", + "production_variants": [ + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 0, + "instance_type": "ml.g4dn.xlarge", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "", + "routing_config": [ + { + "routing_strategy": "LEAST_OUTSTANDING_REQUESTS" + } + ], + "serverless_config": [], + "variant_name": "AllTraffic", + "volume_size_in_gb": 0 + } + ], + "region": "us-east-1", + "shadow_production_variants": [], + "tags": null, + "tags_all": {} + }, + "component_idle": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint-config/c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "async_inference_config": [], + "data_capture_config": [], + "execution_role_arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-bold-fowl", + "id": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "kms_key_arn": "", + "name": "c7n-em-ic-idle-b15118c942ab694e098eeaf08c", + "name_prefix": "c7n-em-ic-idle-", + "production_variants": [ + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 0, + "instance_type": "ml.c5.large", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "", + "routing_config": [ + { + "routing_strategy": "LEAST_OUTSTANDING_REQUESTS" + } + ], + "serverless_config": [], + "variant_name": "AllTraffic", + "volume_size_in_gb": 0 + } + ], + "region": "us-east-1", + "shadow_production_variants": [], + "tags": null, + "tags_all": {} + }, + "idle": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint-config/c7n-em-idle-900bb2c3ad790e42e00a88255f", + "async_inference_config": [], + "data_capture_config": [], + "execution_role_arn": "", + "id": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "kms_key_arn": "", + "name": "c7n-em-idle-900bb2c3ad790e42e00a88255f", + "name_prefix": "c7n-em-idle-", + "production_variants": [ + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 1, + "instance_type": "ml.c5.large", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "c7n-endpoint-metrics-bold-fowl", + "routing_config": [], + "serverless_config": [], + "variant_name": "AllTraffic", + "volume_size_in_gb": 0 + } + ], + "region": "us-east-1", + "shadow_production_variants": [], + "tags": null, + "tags_all": {} + } + }, + "aws_sagemaker_model": { + "main": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:model/c7n-endpoint-metrics-bold-fowl", + "container": [], + "enable_network_isolation": false, + "execution_role_arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-bold-fowl", + "id": "c7n-endpoint-metrics-bold-fowl", + "inference_execution_config": [], + "name": "c7n-endpoint-metrics-bold-fowl", + "primary_container": [ + { + "additional_model_data_source": [], + "container_hostname": "", + "environment": null, + "image": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "image_config": [], + "inference_specification_name": "", + "mode": "SingleModel", + "model_data_source": [ + { + "s3_data_source": [ + { + "compression_type": "Gzip", + "model_access_config": [], + "s3_data_type": "S3Object", + "s3_uri": "s3://c7n-sagemaker-endpoint-metrics-bold-fowl/model.tar.gz" + } + ] + } + ], + "model_data_url": "s3://c7n-sagemaker-endpoint-metrics-bold-fowl/model.tar.gz", + "model_package_name": "", + "multi_model_config": [] + } + ], + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "vpc_config": [] + } + }, + "random_pet": { + "main": { + "id": "bold-fowl", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tests/test_sagemaker.py b/tests/test_sagemaker.py index f8191fd19a4..e9cacf5a2f3 100644 --- a/tests/test_sagemaker.py +++ b/tests/test_sagemaker.py @@ -1,11 +1,17 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import time +from unittest import mock + import pytest from pytest_terraform import terraform from .common import BaseTest -from c7n.resources.sagemaker import SagemakerJobQueryParser, CompilationJobQueryParser +from c7n.filters.metrics import MetricsFilter +from c7n.resources.sagemaker import ( + SAGEMAKER_METRICS, SagemakerEndpoint, SagemakerJobQueryParser, + CompilationJobQueryParser) from c7n.exceptions import PolicyValidationError import botocore.exceptions as b_exc @@ -1605,3 +1611,643 @@ def test_sagemaker_app(test, sagemaker_studio): ) [resource] = p.run() assert resource['AppName'] == sagemaker_studio['aws_sagemaker_app.untagged.app_name'] + + +def capture_dimensions(): + """Capture the dimensions of each GetMetricStatistics call. + + Flight data is matched on the api call name alone, so asserting on the + resources a policy returns says nothing about the dimensions it asked + cloudwatch for -- which is the whole of what these filters do. + """ + dimensions = [] + get_metric_data = MetricsFilter.get_metric_data + + def record(self, client, params): + dimensions.append(params['Dimensions']) + return get_metric_data(self, client, params) + + return dimensions, mock.patch.object( + MetricsFilter, 'get_metric_data', record) + +################################################################################ +# +# Tests that test that we actually fetch correct data by running +# policies that depend on it. +# Two policies, each run against both kinds of endpoint: +# +# test policy endpoints +# ------------------------------- ---------------------- ------------------- +# idle Invocations Sum lte 0 two classic +# inference_component the same policy component +# idle_component the same policy component, uncalled +# utilization CPUUtilization Average classic +# less-than 400 +# inference_component_utilization the same policy component +# +# idle_component is why the idle policy needs no missing-value: an endpoint +# nothing has called reports a zero for each interval rather than nothing +# at all, whichever way it hosts its models. What a missing value is for -- +# a metric with no values to compare -- is covered by the two unit tests +# below it, which don't need an endpoint. +# +# The catalogue tests further down cover which metrics can be asked for and +# whether their dimension sets carry data. These cover what a policy +# decides once it has the data. +# + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metrics_idle(test, sagemaker_endpoint_metrics): + # the busy endpoint's invocations land on its second variant, so it is + # only distinguishable from the idle endpoint if every variant is queried + busy = sagemaker_endpoint_metrics['aws_sagemaker_endpoint.busy.name'] + idle = sagemaker_endpoint_metrics['aws_sagemaker_endpoint.idle.name'] + factory = test.replay_flight_data( + 'test_sagemaker_endpoint_metrics_idle') + + if test.recording: + runtime = factory().client('sagemaker-runtime') + for _ in range(5): + runtime.invoke_endpoint( + EndpointName=busy, + TargetVariant='busy', + ContentType='text/csv', + Body='1.0', + ) + time.sleep(300) + + p = test.load_policy( + { + 'name': 'sagemaker-endpoints-idle', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'type': 'value', 'key': 'EndpointName', + 'op': 'in', 'value': [busy, idle]}, + {'type': 'metrics', + 'name': 'Invocations', + 'statistics': 'Sum', + 'days': 1, + 'period': 86400, + 'value': 0, + 'op': 'lte'}, + ], + }, + session_factory=factory, + ) + dimensions, capture = capture_dimensions() + with capture: + [resource] = p.run() + assert resource['EndpointName'] == idle + # the gpu variant is never queried: busy's second variant already + # fails the condition, which settles the endpoint + assert [[d['Value'] for d in dims] for dims in dimensions] == [ + [busy, 'quiet'], [busy, 'busy'], [idle, 'AllTraffic']] + assert [d['Name'] for d in dimensions[0]] == ['EndpointName', 'VariantName'] + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metrics_utilization(test, sagemaker_endpoint_metrics): + # instance utilization metrics are in a namespace of their own, and are + # reported by every variant whether or not it is being invoked + busy = sagemaker_endpoint_metrics['aws_sagemaker_endpoint.busy.name'] + factory = test.replay_flight_data( + 'test_sagemaker_endpoint_metrics_utilization') + + p = test.load_policy( + { + 'name': 'sagemaker-endpoints-underused', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'EndpointName': busy}, + {'type': 'metrics', + 'name': 'CPUUtilization', + 'statistics': 'Average', + 'days': 1, + 'period': 3600, + 'value': 400, + 'op': 'less-than'}, + ], + }, + session_factory=factory, + ) + dimensions, capture = capture_dimensions() + with capture: + [resource] = p.run() + assert resource['EndpointName'] == busy + assert [[d['Value'] for d in dims] for dims in dimensions] == [ + [busy, 'quiet'], [busy, 'busy'], [busy, 'gpu']] + # each variant's series is annotated separately, named by its dimensions + annotated = resource['c7n.metrics'] + assert sorted(key.split('.')[-1] for key in annotated) == [ + 'VariantName=busy', 'VariantName=gpu', 'VariantName=quiet'] + assert all(points for points in annotated.values()) + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metrics_inference_component( + test, sagemaker_endpoint_metrics): + # this endpoint's variant hosts no model -- the model arrives as an + # inference component, and its invocations are published under the + # component's name with no EndpointName dimension anywhere. Querying + # the variant returns nothing, which an idle policy would read as idle. + endpoint = sagemaker_endpoint_metrics.outputs[ + 'component_endpoint_name']['value'] + component = sagemaker_endpoint_metrics.outputs['component_name']['value'] + factory = test.replay_flight_data( + 'test_sagemaker_endpoint_metrics_inference_component') + + if test.recording: + runtime = factory().client('sagemaker-runtime') + for _ in range(5): + runtime.invoke_endpoint( + EndpointName=endpoint, + InferenceComponentName=component, + ContentType='text/csv', + Body='1.0', + ) + time.sleep(300) + + p = test.load_policy( + { + 'name': 'sagemaker-endpoints-idle', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'EndpointName': endpoint}, + {'type': 'metrics', + 'name': 'Invocations', + 'statistics': 'Sum', + 'days': 1, + 'period': 86400, + 'value': 0, + 'op': 'lte'}, + ], + }, + session_factory=factory, + ) + dimensions, capture = capture_dimensions() + with capture: + resources = p.run() + + # the endpoint is serving traffic, so an idle policy must skip it + assert resources == [] + # and it must have asked about the component, not the variant + assert [[(d['Name'], d['Value']) for d in dims] for dims in dimensions] == [ + [('InferenceComponentName', component)]] + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metrics_idle_component( + test, sagemaker_endpoint_metrics): + # a component endpoint nobody has called still publishes a zero for + # each interval, the same as a classic one, so an idle policy finds it + # from its own values and doesn't need a missing value to do it. + endpoint = sagemaker_endpoint_metrics.outputs[ + 'idle_component_endpoint_name']['value'] + factory = test.replay_flight_data( + 'test_sagemaker_endpoint_metrics_idle_component') + + idle = { + 'name': 'sagemaker-endpoints-idle', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'EndpointName': endpoint}, + {'type': 'metrics', + 'name': 'Invocations', + 'statistics': 'Sum', + 'days': 1, + 'period': 86400, + 'value': 0, + 'op': 'lte'}, + ], + } + p = test.load_policy(idle, session_factory=factory) + [resource] = p.run() + assert resource['EndpointName'] == endpoint + # a published zero, not an absence of data + [points] = resource['c7n.metrics'].values() + assert points and all(point['Sum'] == 0.0 for point in points) + + # so a missing value would make no difference to it. The same query + # again, hence the one recorded response serving both runs + idle['filters'][1]['missing-value'] = 0 + p = test.load_policy(idle, session_factory=factory) + [resource] = p.run() + assert resource['EndpointName'] == endpoint + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metrics_inference_component_utilization( + test, sagemaker_endpoint_metrics): + # utilization stays with the variant on a component-hosting endpoint, + # even though its invocations moved to the component -- the namespace + # decides the sub unit, not the endpoint + endpoint = sagemaker_endpoint_metrics.outputs[ + 'component_endpoint_name']['value'] + factory = test.replay_flight_data( + 'test_sagemaker_endpoint_metrics_inference_component_utilization') + + p = test.load_policy( + { + 'name': 'sagemaker-endpoints-underused', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'EndpointName': endpoint}, + {'type': 'metrics', + 'name': 'CPUUtilization', + 'statistics': 'Average', + 'days': 1, + 'period': 3600, + 'value': 400, + 'op': 'less-than'}, + ], + }, + session_factory=factory, + ) + dimensions, capture = capture_dimensions() + with capture: + [resource] = p.run() + + assert resource['EndpointName'] == endpoint + assert [[d['Name'] for d in dims] for dims in dimensions] == [ + ['EndpointName', 'VariantName']] + +# +################################################################################ + + +def test_sagemaker_endpoint_metrics_dimensions_validated(test): + # a dimension aws never publishes this metric under is a policy error, + # rather than a query that quietly returns nothing + policy = { + 'name': 'sagemaker-endpoints-idle', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'type': 'metrics', + 'name': 'Invocations', + 'value': 0, + 'dimensions': {'QueueName': 'nope'}}, + ], + } + with pytest.raises(PolicyValidationError) as caught: + test.load_policy(policy, validate=True) + assert "can't use dimensions ['QueueName']" in str(caught.value) + + # the documentation lists instance type, but the only sets carrying it + # also carry AvailabilityZone and Region, which no policy can supply, + # so it isn't in the catalogue and naming it is an error too + policy['filters'][0]['dimensions'] = {'InstanceType': 'ml.m5.large'} + with pytest.raises(PolicyValidationError) as caught: + test.load_policy(policy, validate=True) + assert "can't use dimensions ['InstanceType']" in str(caught.value) + + # the namespace follows from the metric name, so naming it is an error + del policy['filters'][0]['dimensions'] + policy['filters'][0]['namespace'] = '/aws/sagemaker/Endpoints' + with pytest.raises(PolicyValidationError) as caught: + test.load_policy(policy, validate=True) + assert 'determines the namespace' in str(caught.value) + + # and keys of the shared schema this filter doesn't implement are + # refused rather than ignored + del policy['filters'][0]['namespace'] + policy['filters'][0]['percent-attr'] = 'InstanceCount' + with pytest.raises(PolicyValidationError) as caught: + test.load_policy(policy, validate=True) + assert "doesn't support percent-attr" in str(caught.value) + + # naming the endpoint is allowed -- it is part of the dimension set a + # classic endpoint's metrics are published under + del policy['filters'][0]['percent-attr'] + policy['filters'][0]['dimensions'] = {'EndpointName': 'an-endpoint'} + test.load_policy(policy, validate=True) + + # but not alongside a component, because no set carries both, and + # sending both would ask cloudwatch for a series it never publishes + policy['filters'][0]['dimensions']['InferenceComponentName'] = 'a-component' + with pytest.raises(PolicyValidationError) as caught: + test.load_policy(policy, validate=True) + assert ("can't use dimensions ['EndpointName', 'InferenceComponentName']" + in str(caught.value)) + + # a metric the documentation doesn't describe fails while the policy + # is loading, rather than as an empty report later + del policy['filters'][0]['dimensions'] + policy['filters'][0]['name'] = 'Invocation' + with pytest.raises(AssertionError) as caught: + test.load_policy(policy, validate=True) + assert 'no documented sagemaker-endpoint metric named Invocation' in str( + caught.value) + + +def test_sagemaker_endpoint_metrics_resource_dimension_never_queried(test): + # validation refuses EndpointName alongside a component, so this is the + # belt-and-braces half: were such a filter built without validating, + # the two still mustn't reach cloudwatch together, which publishes no + # such series and would report the endpoint as having no data at all + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + klass = SagemakerEndpoint.filter_registry.get('metrics') + f = klass( + {'type': 'metrics', 'name': 'Invocations', 'statistics': 'Sum', + 'value': 0, 'op': 'lte', + 'dimensions': {'InferenceComponentName': 'comp', + 'EndpointName': 'e'}}, + policy.resource_manager) + f.endpoint_components = {'e': ['comp']} + endpoint = {'EndpointName': 'e', + 'ProductionVariants': [{'VariantName': 'AllTraffic'}]} + + assert f.get_resource_dimension_names(endpoint) is None + assert f.get_dimensions_set(endpoint) == [] + + +def test_sagemaker_metrics_missing_value(test): + # A metric with no values over the window leaves nothing to compare the + # condition against. The missing value stands in for them, and without + # one the endpoint is passed over rather than guessed about. + from c7n.resources.sagemaker import SageMakerMetricsFilter + + def no_data(self, client, params): + return [] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', no_data) + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + metrics_filter = SagemakerEndpoint.filter_registry.get('metrics') + + def selected(**extra): + """Does an idle-endpoint policy select an endpoint?""" + # a fresh endpoint each time: the annotation is also the cache, so + # one that has been through the filter isn't queried again + endpoint = {'EndpointName': 'e', + 'ProductionVariants': [{'VariantName': 'AllTraffic'}]} + f = metrics_filter( + dict(type='metrics', name='Invocations', statistics='Sum', + value=0, op='lte', **extra), + policy.resource_manager) + f.endpoint_components = {} # a classic endpoint + return f.process([endpoint]) == [endpoint] + + assert selected(**{'missing-value': 0}) + assert not selected() + + def reports_invocations(self, client, params): + return [{'Sum': 5}] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', reports_invocations) + assert not selected() + assert not selected(**{'missing-value': 0}) + + def zero(self, client, params): + return [{'Sum': 0}] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', zero) + assert selected() + assert selected(**{'missing-value': 0}) + + +def test_sagemaker_metrics_percentile_statistics(test): + # a percentile is requested as ExtendedStatistics and comes back nested + # under that key, rather than beside Timestamp like a standard statistic + from c7n.resources.sagemaker import SageMakerMetricsFilter + + class OneSubUnit(SageMakerMetricsFilter): + + def get_dimensions_set(self, resource): + return [{'D': 'only'}] + + requested = [] + + def get_metric_data(self, client, params): + requested.append(params) + return [{'Timestamp': 'when', 'Unit': 'Percent', + 'ExtendedStatistics': {'p95': 3.1}}] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', get_metric_data) + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + f = OneSubUnit( + {'type': 'metrics', 'name': 'CPUUtilization', 'statistics': 'p95', + 'value': 50, 'op': 'less-than'}, policy.resource_manager) + resource = {'EndpointName': 'e'} + + assert f.process([resource]) == [resource] + # asked for the percentile as an extended statistic + assert requested[0]['ExtendedStatistics'] == ['p95'] + assert 'Statistics' not in requested[0] + # and the annotation holds the unwrapped values, not the nesting + [points] = resource['c7n.metrics'].values() + assert points == [{'p95': 3.1}] + + +def test_sagemaker_metrics_stop_fetching_once_a_value_fails(test): + # a resource with several sub units costs a call each, and one failing + # value settles it, so the rest are never fetched + from c7n.resources.sagemaker import SageMakerMetricsFilter + + class ThreeSubUnits(SageMakerMetricsFilter): + + def get_dimensions_set(self, resource): + return [{'D': str(i)} for i in range(3)] + + requested = [] + + def get_metric_data(self, client, params): + value = params['Dimensions'][0]['Value'] + requested.append(value) + # the first sub unit fails the condition, the others would pass + return [{'Average': 100 if value == '0' else 1}] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', get_metric_data) + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + f = ThreeSubUnits( + {'type': 'metrics', 'name': 'CPUUtilization', 'value': 50, + 'op': 'less-than'}, policy.resource_manager) + assert f.process([{'EndpointName': 'e'}]) == [] + assert requested == ['0'] + + +def test_sagemaker_endpoint_metrics_variant_without_components(test): + # an endpoint that hosts components reports its invocations against + # them, so naming one of its variants that hosts none leaves nothing + # to measure. Deciding that from the components left after the + # dimensions are applied would make the endpoint look classic and ask + # for a variant's invocations, which it never publishes. + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + klass = SagemakerEndpoint.filter_registry.get('metrics') + f = klass( + {'type': 'metrics', 'name': 'Invocations', 'statistics': 'Sum', + 'value': 0, 'op': 'lte', 'dimensions': {'VariantName': 'quiet'}}, + policy.resource_manager) + f.endpoint_components = {'e': ['component']} + resource = {'EndpointName': 'e', + 'ProductionVariants': [{'VariantName': 'busy'}, + {'VariantName': 'quiet'}]} + assert f.get_dimensions_set(resource) == [] + + +# Metrics the endpoints in tests/terraform/sagemaker_endpoint_metrics don't +# produce, so there's nothing to record for them. The streaming metrics need +# a container that streams responses, ModelSetupTime a model load, and the +# multi-model metrics an endpoint hosting a model in MultiModel mode. +UNEXERCISED_METRICS = frozenset(( + 'ModelSetupTime', + 'MidStreamErrors', + 'FirstChunkLatency', + 'FirstChunkModelLatency', + 'FirstChunkOverheadLatency', + 'ModelLoadingWaitTime', + 'ModelUnloadingTime', + 'ModelDownloadingTime', + 'ModelLoadingTime', + 'ModelCacheHit', + 'LoadedModelCount', + )) + + +def sagemaker_endpoint_metric_entries(): + """Every (kind, metric) the catalogue describes for endpoints. + + Only the kinds an endpoint can be: metrics filed under no kind are + loaded into every kind, so they show up under each of these. + """ + return [ + pytest.param(kind, metric, published, id=f"{kind}-{metric}") + for kind, by_resource in SAGEMAKER_METRICS.items() + if kind is not None + for metric, published in by_resource['sagemaker-endpoint'].items() + ] + + +def sagemaker_endpoint_metric_entries_published(): + """The entries these endpoints can be recorded against.""" + return [ + entry if entry.values[1] not in UNEXERCISED_METRICS else + pytest.param(*entry.values, id=entry.id, + marks=pytest.mark.skip( + reason='not produced by these endpoints')) + for entry in sagemaker_endpoint_metric_entries() + ] + + +@pytest.mark.parametrize('kind,metric,published', + sagemaker_endpoint_metric_entries()) +def test_sagemaker_endpoint_metric_entry(test, kind, metric, published): + # every entry the catalogue describes has to resolve, for the kind of + # endpoint it's filed under, to that entry's namespace and to + # dimensions the filter can actually supply + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + klass = SagemakerEndpoint.filter_registry.get('metrics') + f = klass({'type': 'metrics', 'name': metric, 'value': 0}, + policy.resource_manager) + f.validate() + + endpoint = {'EndpointName': 'e', + 'ProductionVariants': [{'VariantName': 'AllTraffic'}]} + f.endpoint_components = ( + {'e': ['component']} if kind == 'inference-component' else {}) + assert f.resource_kind(endpoint) == kind + + assert f.get_resource_namespace(endpoint) == published['namespace'] + + dimensions_set = f.get_dimensions_set(endpoint) + assert dimensions_set, f"{metric} resolved to no dimensions" + assert {tuple(sorted(dimensions)) for dimensions in dimensions_set} == { + names for names in published['dimension_sets']} + assert all(isinstance(value, str) + for dimensions in dimensions_set + for value in dimensions.values()) + + +# Metrics publish a minute or so after an invocation, and one publication +# window serves every case below, so the traffic is generated once for the +# whole module rather than per test. +INVOKED: list[str] = [] + +# variants and components of the endpoints tests/terraform/ +# sagemaker_endpoint_metrics builds. "quiet" is deliberately left silent. +ENDPOINT_VARIANTS = {'busy': ('quiet', 'busy', 'gpu'), + 'component': ('AllTraffic',)} + + +def invoke_for_metrics(test, factory, endpoints, components): + """Give every endpoint something to report, once per recording run.""" + if not test.recording or INVOKED: + return + INVOKED.append('done') + + runtime = factory().client('sagemaker-runtime') + for name, endpoint in endpoints.items(): + targets = [{'InferenceComponentName': component} + for component in components.get(endpoint, ())] or [ + {'TargetVariant': variant} + for variant in ENDPOINT_VARIANTS[name] + if variant != 'quiet' + ] + for target in targets: + for _ in range(5): + runtime.invoke_endpoint( + EndpointName=endpoint, ContentType='text/csv', + Body='1.0', **target) + time.sleep(300) + + +@pytest.mark.audited +@pytest.mark.parametrize('kind,metric,published', + sagemaker_endpoint_metric_entries_published()) +@terraform('sagemaker_endpoint_metrics', scope='module') +def test_sagemaker_endpoint_metric_published( + test, sagemaker_endpoint_metrics, kind, metric, published): + # every dimension set the catalogue names for a kind of endpoint has to + # carry data for an endpoint of that kind. A set that isn't published + # returns nothing, which no policy can tell from a quiet endpoint. + endpoints = { + name: sagemaker_endpoint_metrics[f'aws_sagemaker_endpoint.{name}.name'] + for name in ENDPOINT_VARIANTS + } + component = sagemaker_endpoint_metrics.outputs['component_name']['value'] + components = {endpoints['component']: [component]} + + factory = test.replay_flight_data( + f"test_sagemaker_metric_{kind.replace('-', '_')}_{metric}") + invoke_for_metrics(test, factory, endpoints, components) + + hosting = 'component' if kind == 'inference-component' else 'busy' + resource = { + 'EndpointName': endpoints[hosting], + 'ProductionVariants': [{'VariantName': variant} + for variant in ENDPOINT_VARIANTS[hosting]], + } + + p = test.load_policy( + {'name': 'sagemaker-endpoint-metric', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'type': 'metrics', 'name': metric, 'statistics': 'Average', + 'days': 1, 'period': 3600, 'value': 0, 'op': 'gte'}, + ]}, + session_factory=factory, + ) + f = p.resource_manager.filters[-1] + # the components come from the module rather than a ListInferenceComponents + # call, so each recording holds just the CloudWatch request + f.endpoint_components = components + + assert f.process([resource]) == [resource] + + # one series per dimension set, and at least one carrying data: a + # GPU metric has nothing for a variant with no GPU, which is why this + # isn't every series + series = resource['c7n.metrics'] + assert len(series) == len(f.get_dimensions_set(resource)) + assert any(points for points in series.values()), ( + f"{metric} returned no data for any {kind} dimension set") From ac8d1205909bf458e8a56c90ef509ae2d79c36dd Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:52:47 -0500 Subject: [PATCH 27/65] gcp - kms-cryptokey - add set-iam-policy action (#11150) --- tools/c7n_gcp/c7n_gcp/resources/kms.py | 31 +++++ ...c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json | 45 +++++++ ...c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json | 68 ++++++++++ ...-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json | 45 +++++++ ...keyRings-c7n-kr-002c1dda-cryptoKeys_1.json | 114 ++++++++++++++++ ...eyRings-c7n-kr-drea-5734-cryptoKeys_1.json | 123 ++++++++++++++++++ ...s-c7n-test-keyring-bunny-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-firefly-cryptoKeys_1.json | 46 +++++++ ...eys-c7n-test-cryptokey-getIamPolicy_1.json | 35 +++++ ...eys-c7n-test-cryptokey-getIamPolicy_2.json | 41 ++++++ ...-c7n-test-keyring-gibbon-cryptoKeys_1.json | 43 ++++++ ...s-c7n-test-keyring-hyena-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-katydid-cryptoKeys_1.json | 46 +++++++ ...dian-locations-us-central1-keyRings_1.json | 62 +++++++++ ...eys-c7n-test-cryptokey-setIamPolicy_1.json | 40 ++++++ ...c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json | 45 +++++++ ...c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json | 68 ++++++++++ ...-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json | 45 +++++++ ...keyRings-c7n-kr-002c1dda-cryptoKeys_1.json | 114 ++++++++++++++++ ...eyRings-c7n-kr-drea-5734-cryptoKeys_1.json | 123 ++++++++++++++++++ ...s-c7n-test-keyring-bunny-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-firefly-cryptoKeys_1.json | 46 +++++++ ...eys-c7n-test-cryptokey-getIamPolicy_1.json | 35 +++++ ...eys-c7n-test-cryptokey-getIamPolicy_2.json | 35 +++++ ...eys-c7n-test-cryptokey-getIamPolicy_3.json | 29 +++++ ...-c7n-test-keyring-gibbon-cryptoKeys_1.json | 43 ++++++ ...s-c7n-test-keyring-hyena-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-katydid-cryptoKeys_1.json | 46 +++++++ ...dian-locations-us-central1-keyRings_1.json | 62 +++++++++ ...eys-c7n-test-cryptokey-setIamPolicy_1.json | 28 ++++ ...c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json | 45 +++++++ ...c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json | 68 ++++++++++ ...-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json | 45 +++++++ ...keyRings-c7n-kr-002c1dda-cryptoKeys_1.json | 114 ++++++++++++++++ ...eyRings-c7n-kr-drea-5734-cryptoKeys_1.json | 123 ++++++++++++++++++ ...s-c7n-test-keyring-bunny-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-firefly-cryptoKeys_1.json | 46 +++++++ ...eys-c7n-test-cryptokey-getIamPolicy_1.json | 41 ++++++ ...eys-c7n-test-cryptokey-getIamPolicy_2.json | 35 +++++ ...-c7n-test-keyring-gibbon-cryptoKeys_1.json | 43 ++++++ ...s-c7n-test-keyring-hyena-cryptoKeys_1.json | 46 +++++++ ...c7n-test-keyring-katydid-cryptoKeys_1.json | 46 +++++++ ...dian-locations-us-central1-keyRings_1.json | 62 +++++++++ ...eys-c7n-test-cryptokey-setIamPolicy_1.json | 34 +++++ .../kms_cryptokey_set_iam_policy/main.tf | 32 +++++ .../tf_resources.json | 93 +++++++++++++ tools/c7n_gcp/tests/test_kms.py | 101 ++++++++++++++ 47 files changed, 2662 insertions(+) create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-add/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_3.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/kms.py b/tools/c7n_gcp/c7n_gcp/resources/kms.py index c7584e6f133..adc932b9488 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/kms.py +++ b/tools/c7n_gcp/c7n_gcp/resources/kms.py @@ -254,6 +254,37 @@ def _verb_arguments(self, resource): return verb_arguments +@KmsCryptoKey.action_registry.register('set-iam-policy') +class KmsCryptoKeySetIamPolicy(SetIamPolicy): + """Sets IAM policy bindings on a gcp.kms-cryptokey resource. + + GCP action is https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys/setIamPolicy + + :Example: + + .. code-block:: yaml + + policies: + - name: gcp-kms-cryptokey-remove-public-access + resource: gcp.kms-cryptokey + filters: + - type: iam-policy + user-role: + role: + op: glob + value: '*' + user: + op: in + value: + - allUsers + - allAuthenticatedUsers + actions: + - type: set-iam-policy + remove-bindings: matched + """ + permissions = ('cloudkms.cryptoKeys.getIamPolicy', 'cloudkms.cryptoKeys.setIamPolicy') + + @resources.register('kms-cryptokey-version') class KmsCryptoKeyVersion(ChildResourceManager): """GCP Resource diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json new file mode 100644 index 00000000000..300d589e33b --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1048", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.104595530Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.104595530Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:10:15.175829584Z", + "nextRotationTime": "2026-12-21T18:54:14.104595Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json new file mode 100644 index 00000000000..f623470add4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json @@ -0,0 +1,68 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2015", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101416011Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101416011Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:12:35.921081125Z", + "nextRotationTime": "2026-12-21T18:54:14.101416Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101946510Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101946510Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T18:21:19.203769959Z", + "nextRotationTime": "2026-12-21T18:54:14.101946Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 2 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json new file mode 100644 index 00000000000..29189f9b819 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1008", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.102414216Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.102414216Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-04T19:43:34.626576729Z", + "nextRotationTime": "2026-12-21T18:54:14.102414Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json new file mode 100644 index 00000000000..082d0736759 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json @@ -0,0 +1,114 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3993", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101422915Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101422915Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.772547372Z", + "nextRotationTime": "2026-12-21T18:54:14.101422Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101644695Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101644695Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.771892502Z", + "nextRotationTime": "2026-12-21T18:54:14.101644Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda/cryptoKeyVersions/9", + "state": "ENABLED", + "createTime": "2026-09-16T21:33:27.464040758Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-16T21:33:27.464040758Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.776168956Z", + "nextRotationTime": "2026-10-16T21:33:27.464040Z", + "rotationPeriod": "2592000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda/cryptoKeyVersions/3", + "state": "ENABLED", + "createTime": "2026-07-18T21:33:26.844746495Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-18T21:33:26.844746495Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.717394858Z", + "nextRotationTime": "2026-10-16T21:33:26.844746Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 4 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json new file mode 100644 index 00000000000..f347cc75929 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json @@ -0,0 +1,123 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "4373", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.192000741Z", + "destroyTime": "2026-10-15T18:56:07.619116Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.192000741Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.192000741Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-asym-drea-5734", + "purpose": "ASYMMETRIC_SIGN", + "createTime": "2026-09-15T18:53:21.331948677Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "RSA_SIGN_PKCS1_2048_SHA256" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.341384980Z", + "destroyTime": "2026-10-15T18:56:07.654552Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.341384980Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.341384980Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.336337913Z", + "destroyTime": "2026-10-15T18:56:07.820874Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.336337913Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.336337913Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.327953380Z", + "destroyTime": "2026-10-15T18:56:07.751797Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.327953380Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.327953380Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 5 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json new file mode 100644 index 00000000000..65c5c24633d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101821088Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101821088Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T22:00:20.754454946Z", + "nextRotationTime": "2026-12-21T18:54:14.101821Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json new file mode 100644 index 00000000000..0fcf5a9da96 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101976613Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101976613Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:58:08.866000654Z", + "nextRotationTime": "2026-12-21T18:54:14.101976Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json new file mode 100644 index 00000000000..09864689c6c --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "421", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3Iscag=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json new file mode 100644 index 00000000000..18da219ef9f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json @@ -0,0 +1,41 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:55 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "608", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3KZ3C4=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json new file mode 100644 index 00000000000..fa8b3697cc2 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json @@ -0,0 +1,43 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "939", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-09-25T19:08:31.380929898Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-25T19:08:31.380929898Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-25T19:08:31.380929898Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json new file mode 100644 index 00000000000..43ccfed2392 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101662844Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101662844Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:59:00.674996496Z", + "nextRotationTime": "2026-12-21T18:54:14.101662Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json new file mode 100644 index 00000000000..303c1abfe08 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:54 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.106560460Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.106560460Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:54:52.370724478Z", + "nextRotationTime": "2026-12-21T18:54:14.106560Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json new file mode 100644 index 00000000000..7a4adac75b4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json @@ -0,0 +1,62 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:53 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1720", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings?alt=json" + }, + "body": { + "keyRings": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0", + "createTime": "2026-02-05T16:10:14.900501759Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9", + "createTime": "2026-02-05T16:12:35.734409834Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b", + "createTime": "2026-02-04T19:43:34.403709385Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda", + "createTime": "2026-01-19T21:33:26.505895499Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734", + "createTime": "2026-09-15T18:53:20.783602708Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny", + "createTime": "2026-02-18T22:00:20.577263882Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly", + "createTime": "2026-02-18T21:58:08.686384584Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon", + "createTime": "2026-09-25T19:08:31.115750919Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena", + "createTime": "2026-02-18T21:59:00.482630090Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid", + "createTime": "2026-02-18T21:54:52.187411717Z" + } + ], + "totalSize": 10 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json new file mode 100644 index 00000000000..4bd5a231e12 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-add/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json @@ -0,0 +1,40 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:55 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "608", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZcU3KZ3C4=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json new file mode 100644 index 00000000000..e4862d0c4e3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1048", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.104595530Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.104595530Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:10:15.175829584Z", + "nextRotationTime": "2026-12-21T18:54:14.104595Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json new file mode 100644 index 00000000000..1bdcfe8254d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json @@ -0,0 +1,68 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2015", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101416011Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101416011Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:12:35.921081125Z", + "nextRotationTime": "2026-12-21T18:54:14.101416Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101946510Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101946510Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T18:21:19.203769959Z", + "nextRotationTime": "2026-12-21T18:54:14.101946Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 2 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json new file mode 100644 index 00000000000..311fcdefad3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1008", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.102414216Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.102414216Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-04T19:43:34.626576729Z", + "nextRotationTime": "2026-12-21T18:54:14.102414Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json new file mode 100644 index 00000000000..5fbd80c2d30 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json @@ -0,0 +1,114 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3993", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101422915Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101422915Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.772547372Z", + "nextRotationTime": "2026-12-21T18:54:14.101422Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101644695Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101644695Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.771892502Z", + "nextRotationTime": "2026-12-21T18:54:14.101644Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda/cryptoKeyVersions/9", + "state": "ENABLED", + "createTime": "2026-09-16T21:33:27.464040758Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-16T21:33:27.464040758Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.776168956Z", + "nextRotationTime": "2026-10-16T21:33:27.464040Z", + "rotationPeriod": "2592000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda/cryptoKeyVersions/3", + "state": "ENABLED", + "createTime": "2026-07-18T21:33:26.844746495Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-18T21:33:26.844746495Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.717394858Z", + "nextRotationTime": "2026-10-16T21:33:26.844746Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 4 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json new file mode 100644 index 00000000000..57bd3655447 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json @@ -0,0 +1,123 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "4373", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.192000741Z", + "destroyTime": "2026-10-15T18:56:07.619116Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.192000741Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.192000741Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-asym-drea-5734", + "purpose": "ASYMMETRIC_SIGN", + "createTime": "2026-09-15T18:53:21.331948677Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "RSA_SIGN_PKCS1_2048_SHA256" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.341384980Z", + "destroyTime": "2026-10-15T18:56:07.654552Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.341384980Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.341384980Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.336337913Z", + "destroyTime": "2026-10-15T18:56:07.820874Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.336337913Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.336337913Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.327953380Z", + "destroyTime": "2026-10-15T18:56:07.751797Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.327953380Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.327953380Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 5 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json new file mode 100644 index 00000000000..b28848500b3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101821088Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101821088Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T22:00:20.754454946Z", + "nextRotationTime": "2026-12-21T18:54:14.101821Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json new file mode 100644 index 00000000000..916f371fd53 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101976613Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101976613Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:58:08.866000654Z", + "nextRotationTime": "2026-12-21T18:54:14.101976Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json new file mode 100644 index 00000000000..12e0796f530 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "430", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3LGl+0=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json new file mode 100644 index 00000000000..12e0796f530 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "430", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3LGl+0=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_3.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_3.json new file mode 100644 index 00000000000..dc6d465e4c6 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_3.json @@ -0,0 +1,29 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "252", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3L1w8U=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json new file mode 100644 index 00000000000..9a85a096dce --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json @@ -0,0 +1,43 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "939", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-09-25T19:08:31.380929898Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-25T19:08:31.380929898Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-25T19:08:31.380929898Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json new file mode 100644 index 00000000000..5675d875d52 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101662844Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101662844Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:59:00.674996496Z", + "nextRotationTime": "2026-12-21T18:54:14.101662Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json new file mode 100644 index 00000000000..ba37017b15b --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.106560460Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.106560460Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:54:52.370724478Z", + "nextRotationTime": "2026-12-21T18:54:14.106560Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json new file mode 100644 index 00000000000..48a420f6c66 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json @@ -0,0 +1,62 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:58 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1720", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings?alt=json" + }, + "body": { + "keyRings": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0", + "createTime": "2026-02-05T16:10:14.900501759Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9", + "createTime": "2026-02-05T16:12:35.734409834Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b", + "createTime": "2026-02-04T19:43:34.403709385Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda", + "createTime": "2026-01-19T21:33:26.505895499Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734", + "createTime": "2026-09-15T18:53:20.783602708Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny", + "createTime": "2026-02-18T22:00:20.577263882Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly", + "createTime": "2026-02-18T21:58:08.686384584Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon", + "createTime": "2026-09-25T19:08:31.115750919Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena", + "createTime": "2026-02-18T21:59:00.482630090Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid", + "createTime": "2026-02-18T21:54:52.187411717Z" + } + ], + "totalSize": 10 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json new file mode 100644 index 00000000000..5a25e0d59c4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-matched/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json @@ -0,0 +1,28 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:09:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "252", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZcU3L1w8U=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json new file mode 100644 index 00000000000..2d01f59599f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-8f5962e0-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1048", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0/cryptoKeys/c7n-dataproc-key-8f5962e0/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.104595530Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.104595530Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:10:15.175829584Z", + "nextRotationTime": "2026-12-21T18:54:14.104595Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json new file mode 100644 index 00000000000..7a63f1ee24f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dataproc-kr-ea33b5a9-cryptoKeys_1.json @@ -0,0 +1,68 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2015", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/c7n-dataproc-key-ea33b5a9/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101416011Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101416011Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T16:12:35.921081125Z", + "nextRotationTime": "2026-12-21T18:54:14.101416Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9/cryptoKeys/my-key/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101946510Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101946510Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-05T18:21:19.203769959Z", + "nextRotationTime": "2026-12-21T18:54:14.101946Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 2 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json new file mode 100644 index 00000000000..bca07435576 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-dp-kr-442b-cryptoKeys_1.json @@ -0,0 +1,45 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1008", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b/cryptoKeys/c7n-dp-key-442b/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.102414216Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.102414216Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-04T19:43:34.626576729Z", + "nextRotationTime": "2026-12-21T18:54:14.102414Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json new file mode 100644 index 00000000000..02f29b38efb --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-002c1dda-cryptoKeys_1.json @@ -0,0 +1,114 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3993", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-365d-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101422915Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101422915Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.772547372Z", + "nextRotationTime": "2026-12-21T18:54:14.101422Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-bad-norot-002c1dda/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101644695Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101644695Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.771892502Z", + "nextRotationTime": "2026-12-21T18:54:14.101644Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-30d-002c1dda/cryptoKeyVersions/9", + "state": "ENABLED", + "createTime": "2026-09-16T21:33:27.464040758Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-16T21:33:27.464040758Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.776168956Z", + "nextRotationTime": "2026-10-16T21:33:27.464040Z", + "rotationPeriod": "2592000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda/cryptoKeys/c7n-key-good-90d-002c1dda/cryptoKeyVersions/3", + "state": "ENABLED", + "createTime": "2026-07-18T21:33:26.844746495Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-07-18T21:33:26.844746495Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-01-19T21:33:26.717394858Z", + "nextRotationTime": "2026-10-16T21:33:26.844746Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 4 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json new file mode 100644 index 00000000000..dc19131260a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-kr-drea-5734-cryptoKeys_1.json @@ -0,0 +1,123 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "4373", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-365d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.192000741Z", + "destroyTime": "2026-10-15T18:56:07.619116Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.192000741Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.192000741Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-asym-drea-5734", + "purpose": "ASYMMETRIC_SIGN", + "createTime": "2026-09-15T18:53:21.331948677Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "RSA_SIGN_PKCS1_2048_SHA256" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-bad-norot-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.341384980Z", + "destroyTime": "2026-10-15T18:56:07.654552Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.341384980Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.341384980Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-30d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.336337913Z", + "destroyTime": "2026-10-15T18:56:07.820874Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.336337913Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.336337913Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734/cryptoKeys/c7n-key-good-90d-drea-5734/cryptoKeyVersions/1", + "state": "DESTROY_SCHEDULED", + "createTime": "2026-09-15T18:53:21.327953380Z", + "destroyTime": "2026-10-15T18:56:07.751797Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-15T18:53:21.327953380Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-15T18:53:21.327953380Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 5 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json new file mode 100644 index 00000000000..c944dcde774 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-bunny-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101821088Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101821088Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T22:00:20.754454946Z", + "nextRotationTime": "2026-12-21T18:54:14.101821Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json new file mode 100644 index 00000000000..91898c5e4f0 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-firefly-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101976613Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101976613Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:58:08.866000654Z", + "nextRotationTime": "2026-12-21T18:54:14.101976Z", + "rotationPeriod": "7776000s", + "labels": { + "goog-terraform-provisioned": "true", + "env": "production" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json new file mode 100644 index 00000000000..4bc2d9962f9 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_1.json @@ -0,0 +1,41 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "608", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3KZ3C4=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json new file mode 100644 index 00000000000..d08dd165342 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-getIamPolicy_2.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:58 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "430", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey:getIamPolicy?alt=json" + }, + "body": { + "version": 1, + "etag": "BwZcU3LGl+0=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json new file mode 100644 index 00000000000..4222e27a7e8 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys_1.json @@ -0,0 +1,43 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "939", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED", + "createTime": "2026-09-25T19:08:31.380929898Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-25T19:08:31.380929898Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-09-25T19:08:31.380929898Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json new file mode 100644 index 00000000000..80ff5197484 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-hyena-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1059", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.101662844Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.101662844Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:59:00.674996496Z", + "nextRotationTime": "2026-12-21T18:54:14.101662Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json new file mode 100644 index 00000000000..3dd51cea0f5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-katydid-cryptoKeys_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1063", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys?alt=json" + }, + "body": { + "cryptoKeys": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey", + "primary": { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/2", + "state": "ENABLED", + "createTime": "2026-09-22T18:54:14.106560460Z", + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "generateTime": "2026-09-22T18:54:14.106560460Z" + }, + "purpose": "ENCRYPT_DECRYPT", + "createTime": "2026-02-18T21:54:52.370724478Z", + "nextRotationTime": "2026-12-21T18:54:14.106560Z", + "rotationPeriod": "7776000s", + "labels": { + "env": "production", + "goog-terraform-provisioned": "true" + }, + "versionTemplate": { + "protectionLevel": "SOFTWARE", + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION" + }, + "destroyScheduledDuration": "2592000s" + } + ], + "totalSize": 1 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json new file mode 100644 index 00000000000..2cf58b49214 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/get-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings_1.json @@ -0,0 +1,62 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:56 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1720", + "-content-encoding": "gzip", + "content-location": "https://cloudkms.googleapis.com/v1/projects/cloud-custodian/locations/us-central1/keyRings?alt=json" + }, + "body": { + "keyRings": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-8f5962e0", + "createTime": "2026-02-05T16:10:14.900501759Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dataproc-kr-ea33b5a9", + "createTime": "2026-02-05T16:12:35.734409834Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-dp-kr-442b", + "createTime": "2026-02-04T19:43:34.403709385Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-002c1dda", + "createTime": "2026-01-19T21:33:26.505895499Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-kr-drea-5734", + "createTime": "2026-09-15T18:53:20.783602708Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-bunny", + "createTime": "2026-02-18T22:00:20.577263882Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-firefly", + "createTime": "2026-02-18T21:58:08.686384584Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon", + "createTime": "2026-09-25T19:08:31.115750919Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-hyena", + "createTime": "2026-02-18T21:59:00.482630090Z" + }, + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-katydid", + "createTime": "2026-02-18T21:54:52.187411717Z" + } + ], + "totalSize": 10 + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json new file mode 100644 index 00000000000..34f5fdee57f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/kms-key-iam-remove/post-cloudkms.googleapis.com-v1-projects-cloud-custodian-locations-us-central1-keyRings-c7n-test-keyring-gibbon-cryptoKeys-c7n-test-cryptokey-setIamPolicy_1.json @@ -0,0 +1,34 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 25 Sep 2026 19:08:57 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "430", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZcU3LGl+0=", + "bindings": [ + { + "role": "roles/cloudkms.cryptoKeyDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "members": [ + "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/main.tf b/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/main.tf new file mode 100644 index 00000000000..926f9804987 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/main.tf @@ -0,0 +1,32 @@ +provider "google" {} + +resource "random_pet" "keyring" { + length = 1 +} + +resource "google_kms_key_ring" "c7n_test" { + name = "c7n-test-keyring-${random_pet.keyring.id}" + location = "us-central1" +} + +resource "google_kms_crypto_key" "c7n_test_key" { + name = "c7n-test-cryptokey" + key_ring = google_kms_key_ring.c7n_test.id + purpose = "ENCRYPT_DECRYPT" +} + +resource "google_service_account" "c7n_test" { + account_id = "c7n-kms-${random_pet.keyring.id}" +} + +resource "google_kms_crypto_key_iam_member" "encrypter" { + crypto_key_id = google_kms_crypto_key.c7n_test_key.id + role = "roles/cloudkms.cryptoKeyEncrypter" + member = "serviceAccount:${google_service_account.c7n_test.email}" +} + +resource "google_kms_crypto_key_iam_member" "decrypter" { + crypto_key_id = google_kms_crypto_key.c7n_test_key.id + role = "roles/cloudkms.cryptoKeyDecrypter" + member = "serviceAccount:${google_service_account.c7n_test.email}" +} diff --git a/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/tf_resources.json b/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/tf_resources.json new file mode 100644 index 00000000000..1fb929ff947 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/kms_cryptokey_set_iam_policy/tf_resources.json @@ -0,0 +1,93 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_kms_crypto_key": { + "c7n_test_key": { + "crypto_key_backend": "", + "deletion_policy": "DELETE", + "destroy_scheduled_duration": "2592000s", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "import_only": false, + "key_ring": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon", + "labels": null, + "name": "c7n-test-cryptokey", + "primary": [ + { + "name": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/cryptoKeyVersions/1", + "state": "ENABLED" + } + ], + "purpose": "ENCRYPT_DECRYPT", + "rotation_period": "", + "skip_initial_version_creation": false, + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "timeouts": null, + "version_template": [ + { + "algorithm": "GOOGLE_SYMMETRIC_ENCRYPTION", + "protection_level": "SOFTWARE" + } + ] + } + }, + "google_kms_crypto_key_iam_member": { + "decrypter": { + "condition": [], + "crypto_key_id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "etag": "BwZcU3Iscag=", + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/roles/cloudkms.cryptoKeyDecrypter/serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "role": "roles/cloudkms.cryptoKeyDecrypter" + }, + "encrypter": { + "condition": [], + "crypto_key_id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey", + "etag": "BwZcU3Iscag=", + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon/cryptoKeys/c7n-test-cryptokey/roles/cloudkms.cryptoKeyEncrypter/serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "role": "roles/cloudkms.cryptoKeyEncrypter" + } + }, + "google_kms_key_ring": { + "c7n_test": { + "id": "projects/cloud-custodian/locations/us-central1/keyRings/c7n-test-keyring-gibbon", + "location": "us-central1", + "name": "c7n-test-keyring-gibbon", + "project": "cloud-custodian", + "timeouts": null + } + }, + "google_service_account": { + "c7n_test": { + "account_id": "c7n-kms-gibbon", + "create_ignore_already_exists": null, + "deletion_policy": "DELETE", + "description": "", + "disabled": false, + "display_name": "", + "email": "c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "id": "projects/cloud-custodian/serviceAccounts/c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "name": "projects/cloud-custodian/serviceAccounts/c7n-kms-gibbon@cloud-custodian.iam.gserviceaccount.com", + "project": "cloud-custodian", + "timeouts": null, + "unique_id": "100000000000000000000" + } + }, + "random_pet": { + "keyring": { + "id": "gibbon", + "keepers": null, + "length": 1, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_kms.py b/tools/c7n_gcp/tests/test_kms.py index f0d438a8b03..f56e09e3934 100644 --- a/tools/c7n_gcp/tests/test_kms.py +++ b/tools/c7n_gcp/tests/test_kms.py @@ -473,3 +473,104 @@ def test_kms_cryptokey_update(test, kms_cryptokey): result = client.execute_query('get', {'name': key_name}) assert result['rotationPeriod'] == '7776000s' assert result['nextRotationTime'] == '2027-01-01T00:00:00Z' + + +# The set-iam-policy tests share one session-scoped fixture because KMS key rings +# and crypto keys can never be deleted, so each fixture apply leaves a key ring +# behind in the project. To stay order-independent, each test owns a distinct +# role: add-bindings adds EncrypterDecrypter, remove-bindings removes Encrypter, +# and remove-bindings: matched removes Decrypter. +def _kms_cryptokey_iam_roles_to_members(policy, key_name): + client = policy.resource_manager.get_client() + result = client.execute_query('getIamPolicy', {'resource': key_name}) + return {b['role']: b['members'] for b in result.get('bindings', [])} + + +@terraform('kms_cryptokey_set_iam_policy', scope='session') +def test_kms_cryptokey_set_iam_policy_add_bindings(test, kms_cryptokey_set_iam_policy): + key_name = kms_cryptokey_set_iam_policy['google_kms_crypto_key.c7n_test_key.id'] + member = 'serviceAccount:%s' % ( + kms_cryptokey_set_iam_policy['google_service_account.c7n_test.email']) + factory = test.replay_flight_data('kms-key-iam-add') + policy = test.load_policy( + { + 'name': 'kms-cryptokey-set-iam-policy-add-bindings', + 'resource': 'gcp.kms-cryptokey', + 'query': [{'location': 'us-central1'}], + 'filters': [{'type': 'value', 'key': 'name', 'value': key_name}], + 'actions': [{ + 'type': 'set-iam-policy', + 'add-bindings': [{ + 'role': 'roles/cloudkms.cryptoKeyEncrypterDecrypter', + 'members': [member], + }], + }], + }, + session_factory=factory, + ) + resources = policy.run() + assert len(resources) == 1 + + roles_to_members = _kms_cryptokey_iam_roles_to_members(policy, key_name) + assert roles_to_members['roles/cloudkms.cryptoKeyEncrypterDecrypter'] == [member] + + +@terraform('kms_cryptokey_set_iam_policy', scope='session') +def test_kms_cryptokey_set_iam_policy_remove_bindings(test, kms_cryptokey_set_iam_policy): + key_name = kms_cryptokey_set_iam_policy['google_kms_crypto_key.c7n_test_key.id'] + member = 'serviceAccount:%s' % ( + kms_cryptokey_set_iam_policy['google_service_account.c7n_test.email']) + factory = test.replay_flight_data('kms-key-iam-remove') + policy = test.load_policy( + { + 'name': 'kms-cryptokey-set-iam-policy-remove-bindings', + 'resource': 'gcp.kms-cryptokey', + 'query': [{'location': 'us-central1'}], + 'filters': [{'type': 'value', 'key': 'name', 'value': key_name}], + 'actions': [{ + 'type': 'set-iam-policy', + 'remove-bindings': [{ + 'role': 'roles/cloudkms.cryptoKeyEncrypter', + 'members': [member], + }], + }], + }, + session_factory=factory, + ) + resources = policy.run() + assert len(resources) == 1 + + roles_to_members = _kms_cryptokey_iam_roles_to_members(policy, key_name) + assert 'roles/cloudkms.cryptoKeyEncrypter' not in roles_to_members + + +@terraform('kms_cryptokey_set_iam_policy', scope='session') +def test_kms_cryptokey_set_iam_policy_remove_matched(test, kms_cryptokey_set_iam_policy): + key_name = kms_cryptokey_set_iam_policy['google_kms_crypto_key.c7n_test_key.id'] + member = 'serviceAccount:%s' % ( + kms_cryptokey_set_iam_policy['google_service_account.c7n_test.email']) + factory = test.replay_flight_data('kms-key-iam-matched') + policy = test.load_policy( + { + 'name': 'kms-cryptokey-set-iam-policy-remove-matched', + 'resource': 'gcp.kms-cryptokey', + 'query': [{'location': 'us-central1'}], + 'filters': [ + {'type': 'value', 'key': 'name', 'value': key_name}, + {'type': 'iam-policy', + 'user-role': { + 'role': {'op': 'eq', 'value': 'roles/cloudkms.cryptoKeyDecrypter'}, + 'user': {'op': 'glob', 'value': 'serviceAccount:*'}, + }}, + ], + 'actions': [{'type': 'set-iam-policy', 'remove-bindings': 'matched'}], + }, + session_factory=factory, + ) + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['c7n:matched-iam-bindings'] == [ + {'role': 'roles/cloudkms.cryptoKeyDecrypter', 'member': member}] + + roles_to_members = _kms_cryptokey_iam_roles_to_members(policy, key_name) + assert 'roles/cloudkms.cryptoKeyDecrypter' not in roles_to_members From 5e2b8faea914edf1ec0551f643f8b8653ee67135 Mon Sep 17 00:00:00 2001 From: James Troup Date: Wed, 7 Oct 2026 18:00:37 +0000 Subject: [PATCH 28/65] aws - securityhub - truncate oversize string detail values (#11161) --- c7n/resources/securityhub.py | 25 +++++++++++++++-- tests/test_securityhub.py | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/c7n/resources/securityhub.py b/c7n/resources/securityhub.py index f698fa3cec6..7abf3ef32ea 100644 --- a/c7n/resources/securityhub.py +++ b/c7n/resources/securityhub.py @@ -322,6 +322,27 @@ class SecurityHubAction(SecurityHub): # Mostly undocumented value size limit SECHUB_VALUE_SIZE_LIMIT = 1024 +# Appended to a detail value cut down to SECHUB_VALUE_SIZE_LIMIT, so that +# someone reading the finding can tell the value was cut. +SECHUB_TRUNCATION_MARKER = '...' + + +def truncate_value(value): + """Cap a finding detail value to the Security Hub per-value size limit. + + Security Hub rejects the whole finding if any detail value is over the + limit, so an oversize value is cut short and marked as such rather than + costing the caller the finding. + + The limit is inclusive. The service rejects with "should NOT be longer + than 1024 characters", so a value of exactly that length is accepted; + the user guide's "fewer than 1,024" is looser than what is enforced. + """ + if len(value) <= SECHUB_VALUE_SIZE_LIMIT: + return value + keep = SECHUB_VALUE_SIZE_LIMIT - len(SECHUB_TRUNCATION_MARKER) + return value[:keep] + SECHUB_TRUNCATION_MARKER + class PostFinding(Action): """Report a finding to AWS Security Hub. @@ -651,9 +672,9 @@ def format_resource(self, r): v = dumps(v) elif isinstance(v, (int, float, bool)): v = str(v) - else: + elif not isinstance(v, str): continue - details[k] = v[:SECHUB_VALUE_SIZE_LIMIT] + details[k] = truncate_value(v) details['c7n:resource-type'] = self.manager.type other = { diff --git a/tests/test_securityhub.py b/tests/test_securityhub.py index e2311cf3c41..940298f4373 100644 --- a/tests/test_securityhub.py +++ b/tests/test_securityhub.py @@ -3,8 +3,14 @@ from c7n.exceptions import PolicyValidationError from c7n.resources.aws import shape_validate +from c7n.resources.securityhub import ( + SECHUB_TRUNCATION_MARKER, + SECHUB_VALUE_SIZE_LIMIT, + truncate_value, +) from .common import BaseTest, event_data +import json import logging import time @@ -130,6 +136,54 @@ def test_s3_bucket_arn(self): {'Name': 'xyz', 'CreationDate': 'xtf'}) self.assertEqual(resource['Id'], "arn:aws:s3:::xyz") + def test_truncate_value(self): + self.assertEqual(truncate_value('short'), 'short') + at_limit = 'x' * SECHUB_VALUE_SIZE_LIMIT + self.assertEqual(truncate_value(at_limit), at_limit) + over_limit = truncate_value('x' * (SECHUB_VALUE_SIZE_LIMIT + 1)) + self.assertEqual( + over_limit, + 'x' * (SECHUB_VALUE_SIZE_LIMIT - len(SECHUB_TRUNCATION_MARKER)) + + SECHUB_TRUNCATION_MARKER) + # Deliberately the literal rather than SECHUB_VALUE_SIZE_LIMIT. The cap + # is the service's, so this pins what Security Hub accepts instead of + # restating whatever the constant currently says. + self.assertEqual(len(over_limit), 1024) + + def test_other_finding_truncates_oversize_string(self): + # A resource policy document arrives as a string, and a real one runs + # well past the value size limit. Security Hub rejects the entire + # finding if it is posted intact. + policy = self.load_policy({ + 'name': 'secret-finding', + 'resource': 'aws.secrets-manager', + 'actions': [ + {'type': 'post-finding', + 'types': [ + "Software and Configuration Checks/AWS Security Best Practices" + ]}]}) + post_finding = policy.resource_manager.actions[0] + resource = post_finding.format_resource({ + 'ARN': 'arn:aws:secretsmanager:us-east-1:644160558196:secret:xyz', + 'Name': 'xyz', + 'Policy': json.dumps({'Statement': ['x' * 2000]}), + }) + details = resource['Details']['Other'] + self.assertEqual(len(details['Policy']), SECHUB_VALUE_SIZE_LIMIT) + self.assertTrue(details['Policy'].endswith(SECHUB_TRUNCATION_MARKER)) + # Values inside the limit are left alone. + self.assertEqual(details['Name'], 'xyz') + + # A value of exactly the limit is accepted by the service, so it is + # passed through rather than shortened. + at_limit = 'y' * SECHUB_VALUE_SIZE_LIMIT + resource = post_finding.format_resource({ + 'ARN': 'arn:aws:secretsmanager:us-east-1:644160558196:secret:xyz', + 'Name': 'xyz', + 'Policy': at_limit, + }) + self.assertEqual(resource['Details']['Other']['Policy'], at_limit) + def test_security_hub_master_filter(self): factory = self.replay_flight_data('test_security_hub_master_filter') policy = self.load_policy( From 4deef785ac6706445656e1e01a3a7368eb3f0c65 Mon Sep 17 00:00:00 2001 From: Nihal Reddy Mandala <269013326+nihalrm@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:07:18 -0500 Subject: [PATCH 29/65] aws - security-group - paginate describe_security_groups (#11176) --- c7n/resources/vpc.py | 15 ++++++++++++++- tests/test_vpc.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py index 65515475aa9..2875aaa0f3f 100644 --- a/c7n/resources/vpc.py +++ b/c7n/resources/vpc.py @@ -886,6 +886,19 @@ def load_resource(self, item): return r +class DescribeSecurityGroups(query.DescribeSource): + + def resources(self, query): + # Without MaxResults, DescribeSecurityGroups returns every group in the + # region in one response, which does not scale to large accounts. + # Paging here rather than in the manager keeps the cache key unchanged + # and leaves by-id lookups alone since EC2 rejects MaxResults alongside + # GroupIds. + query = dict(query or {}) + query.setdefault('MaxResults', 1000) + return super().resources(query) + + @resources.register('security-group') class SecurityGroup(query.QueryResourceManager): @@ -902,7 +915,7 @@ class resource_type(query.TypeInfo): source_mapping = { 'config': ConfigSG, - 'describe': query.DescribeSource + 'describe': DescribeSecurityGroups } diff --git a/tests/test_vpc.py b/tests/test_vpc.py index 2639b754b23..559fc83cdfd 100644 --- a/tests/test_vpc.py +++ b/tests/test_vpc.py @@ -3,6 +3,7 @@ import logging import time from .common import BaseTest, functional, event_data, load_data +from unittest import mock from unittest.mock import MagicMock from botocore.exceptions import ClientError as BotoClientError @@ -4924,3 +4925,41 @@ def test_endpoint_service_details_inaccessible_service_skipped(self): self.assertEqual(len(resources), 1) self.assertEqual(resources[0]["ServiceName"], "com.amazonaws.us-east-1.s3") self.assertTrue(resources[0]["c7n:ServiceDetails"]["VpcEndpointPolicySupported"]) + + +class SecurityGroupPaginationTest(BaseTest): + + def test_describe_sends_max_results(self): + p = self.load_policy({'name': 'sg', 'resource': 'aws.security-group'}) + with mock.patch('c7n.query.ResourceQuery.filter', return_value=[]) as filter_: + p.resource_manager.resources() + self.assertEqual(filter_.call_args.kwargs.get('MaxResults'), 1000) + + def test_get_resources_omits_max_results(self): + # EC2 rejects MaxResults alongside GroupIds and get_resources swallows + # the ClientError so by-id lookups must not be paged. + p = self.load_policy({'name': 'sg', 'resource': 'aws.security-group'}) + with mock.patch('c7n.query.ResourceQuery.filter', return_value=[]) as filter_: + p.resource_manager.get_resources(['sg-0123456789abcdef0'], cache=False) + filter_.assert_called_once() + self.assertEqual(filter_.call_args.kwargs['GroupIds'], ['sg-0123456789abcdef0']) + self.assertNotIn('MaxResults', filter_.call_args.kwargs) + + def test_get_resources_reuses_cached_listing(self): + # Paging must not change the listing's cache key or by-id lookups + # after a full listing miss the cache and call the API again. + p = self.load_policy( + {'name': 'sg', 'resource': 'aws.security-group'}, cache=True) + sg = {'GroupId': 'sg-0123456789abcdef0'} + with mock.patch('c7n.query.ResourceQuery.filter', return_value=[sg]) as filter_: + p.resource_manager.resources() + found = p.resource_manager.get_resources([sg['GroupId']]) + self.assertEqual(found, [sg]) + filter_.assert_called_once() + + def test_config_source_query_untouched(self): + p = self.load_policy( + {'name': 'sg', 'resource': 'aws.security-group', 'source': 'config'}) + with mock.patch('c7n.query.ConfigSource.resources', return_value=[]) as res: + p.resource_manager.resources() + self.assertNotIn('MaxResults', res.call_args.args[0] or {}) From fb151ca235fc1223a777d665ebb3d00a92b6f345 Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Wed, 7 Oct 2026 14:12:07 -0400 Subject: [PATCH 30/65] gcp - organization and project - add dlp-discovery-configs filter (#11179) --- .../c7n_gcp/resources/resourcemanager.py | 169 +++++++++++++ ...emanager.googleapis.com-v1-projects_1.json | 36 +++ ...todian-locations---discoveryConfigs_1.json | 41 ++++ ...todian-locations---discoveryConfigs_2.json | 18 ++ .../project_dlp_discovery_configs/main.tf | 36 +++ .../tf_resources.json | 84 +++++++ tools/c7n_gcp/tests/test_resourcemanager.py | 224 +++++++++++++++++- 7 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_2.json create mode 100644 tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py b/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py index 2fd8fe580ce..62f88a6fc9b 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py +++ b/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py @@ -488,6 +488,175 @@ def get_access_approval(self, resource): return access_approval +class DlpDiscoveryConfigsFilter(ListItemFilter): + """Filter Resources based on Sensitive Data Protection (DLP) discovery configurations + + Only configurations created directly under the resource are listed, from every location. + Folder-scoped configurations are created under the organization, so an organization + lists them but not project-level ones. + + Organizations with no running discovery configuration: + + .. code-block:: yaml + + - name: org-no-running-dlp-discovery-configs + resource: gcp.organization + filters: + - type: dlp-discovery-configs + count: 0 + attrs: + - status: RUNNING + """ + schema = type_schema( + 'dlp-discovery-configs', + attrs={'$ref': '#/definitions/filters_common/list_item_attrs'}, + count={'type': 'number'}, + count_op={'$ref': '#/definitions/filters_common/comparison_operators'} + ) + schema_alias = False + + annotate_items = True + # there is no `dlp.discoveryConfigs.list` IAM permission; DLP checks `dlp.jobTriggers.list`. + permissions = ('dlp.jobTriggers.list',) + component = None + + def get_parent(self, resource): + raise NotImplementedError("subclass responsibility") + + def get_item_values(self, resource): + return self.list_configs(self.component, self.get_parent(resource)) + + def list_configs(self, component, parent): + session = local_session(self.manager.session_factory) + client = session.client('dlp', 'v2', component) + pages = client.execute_paged_query('list', {'parent': f"{parent}/locations/-"}) + configs = [] + for page in pages: + configs.extend(page.get('discoveryConfigs', [])) + return configs + + +@Organization.filter_registry.register('dlp-discovery-configs') +class OrgDlpDiscoveryConfigsFilter(DlpDiscoveryConfigsFilter): + component = 'organizations.locations.discoveryConfigs' + + def get_parent(self, resource): + return resource['name'] + + +@Project.filter_registry.register('dlp-discovery-configs') +class ProjectDlpDiscoveryConfigsFilter(DlpDiscoveryConfigsFilter): + """Filter projects based on Sensitive Data Protection (DLP) discovery configurations + + Lists the project's own configurations from every location. With inherited: true it + also adds, for each discovery type, the closest RUNNING folder or organization + configuration above the project, which is the one Google runs for it. Targets that a + closer configuration already covers are removed, and configurations for other clouds + are not inherited. This needs dlp.jobTriggers.list on the organization and + resourcemanager.projects.get on the project. + + A listed configuration can still profile none of the project's data: project ID + patterns, conditions and configuration errors are not evaluated, and disabled targets + are only excluded if the policy checks for them, as the BigQuery example does. + + Projects with a paused discovery configuration: + + .. code-block:: yaml + + - name: project-paused-dlp-discovery-configs + resource: gcp.project + filters: + - type: dlp-discovery-configs + attrs: + - status: PAUSED + + Projects whose BigQuery data no running configuration covers, counting inherited ones: + + .. code-block:: yaml + + - name: project-no-running-bigquery-discovery + resource: gcp.project + filters: + - type: dlp-discovery-configs + inherited: true + count: 0 + attrs: + - status: RUNNING + - type: value + key: "targets[?bigQueryTarget && bigQueryTarget.disabled == `null`]" + value: not-null + """ + schema = type_schema( + 'dlp-discovery-configs', + rinherit=DlpDiscoveryConfigsFilter.schema, + inherited={'type': 'boolean'} + ) + component = 'projects.locations.discoveryConfigs' + ancestors = None + org_configs = None + + def get_parent(self, resource): + return f"projects/{resource['projectId']}" + + def get_permissions(self): + if self.data.get('inherited'): + return self.permissions + ('resourcemanager.projects.get',) + return self.permissions + + def process(self, resources, event=None): + self.ancestors, self.org_configs = {}, {} + return super().process(resources, event) + + def get_item_values(self, resource): + configs = super().get_item_values(resource) + if self.data.get('inherited'): + configs.extend(self.get_inherited_configs(resource)) + return configs + + def get_ancestors(self, resource): + parent = resource.get('parent') or {} + if parent.get('type') == 'organization': + return [], parent['id'] + if parent.get('type') != 'folder': + return [], None + if parent['id'] not in self.ancestors: + session = local_session(self.manager.session_factory) + client = session.client('cloudresourcemanager', 'v1', 'projects') + ancestry = client.execute_command( + 'getAncestry', {'projectId': resource['projectId']}).get('ancestor', []) + folders = [ + a['resourceId']['id'] for a in ancestry if a['resourceId']['type'] == 'folder'] + # ancestors run from the project up, so the organization is last + self.ancestors[parent['id']] = folders, ancestry[-1]['resourceId']['id'] + return self.ancestors[parent['id']] + + def get_inherited_configs(self, resource): + folders, org_id = self.get_ancestors(resource) + if org_id is None: + return [] + if org_id not in self.org_configs: + self.org_configs[org_id] = self.list_configs( + OrgDlpDiscoveryConfigsFilter.component, f"organizations/{org_id}") + # Google runs only the closest active folder or organization configuration of each + # discovery type for a project, so walk up from the nearest folder and skip types a + # closer configuration has claimed. Other-cloud targets never scan the project. + # https://docs.cloud.google.com/sensitive-data-protection/docs/data-profiles + scopes = [('folderId', f) for f in folders] + [('organizationId', org_id)] + claimed, inherited = set(), [] + for key, scope_id in scopes: + for config in self.org_configs[org_id]: + location = config.get('orgConfig', {}).get('location', {}) + if config.get('status') != 'RUNNING' or location.get(key) != scope_id: + continue + targets = [ + t for t in config.get('targets', []) + if 'otherCloudTarget' not in t and not claimed & set(t)] + if targets: + claimed.update(*targets) + inherited.append(dict(config, targets=targets)) + return inherited + + @Organization.filter_registry.register('iam-policy') class OrganizationIamPolicyFilter(IamPolicyFilter): """ diff --git a/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..535b7d25670 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 02 Oct 2026 14:02:37 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=277", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "393", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?filter=id%3Acloud-custodian&alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "400632884532", + "projectId": "cloud-custodian", + "lifecycleState": "ACTIVE", + "name": "cloud-custodian", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-10T20:26:52.526Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_1.json b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_1.json new file mode 100644 index 00000000000..46f0e7d017f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_1.json @@ -0,0 +1,41 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 02 Oct 2026 14:02:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "746", + "-content-encoding": "gzip", + "content-location": "https://dlp.googleapis.com/v2/projects/cloud-custodian/locations/-/discoveryConfigs?alt=json" + }, + "body": { + "discoveryConfigs": [ + { + "name": "projects/cloud-custodian/locations/us/discoveryConfigs/8338945954561565050", + "inspectTemplates": [ + "projects/cloud-custodian/locations/us/inspectTemplates/4167487095981753467" + ], + "targets": [ + { + "bigQueryTarget": { + "filter": { + "otherTables": {} + } + } + } + ], + "createTime": "2026-10-02T14:02:36.223251Z", + "updateTime": "2026-10-02T14:02:36.223251Z", + "status": "PAUSED", + "displayName": "c7n-dlp-discovery-configs" + } + ], + "nextPageToken": "c7n-page-token" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_2.json b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_2.json new file mode 100644 index 00000000000..887dc65909f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-dlp-discovery-configs/get-dlp.googleapis.com-v2-projects-cloud-custodian-locations---discoveryConfigs_2.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 02 Oct 2026 14:02:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://dlp.googleapis.com/v2/projects/cloud-custodian/locations/-/discoveryConfigs?alt=json&pageToken=c7n-page-token" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/main.tf b/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/main.tf new file mode 100644 index 00000000000..969e75d63db --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/main.tf @@ -0,0 +1,36 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id + billing_project = var.google_project_id + user_project_override = true +} + +resource "google_data_loss_prevention_inspect_template" "c7n" { + parent = "projects/${var.google_project_id}/locations/us" + display_name = "c7n-dlp-discovery-configs" + + inspect_config { + info_types { + name = "EMAIL_ADDRESS" + } + } +} + +resource "google_data_loss_prevention_discovery_config" "c7n" { + parent = "projects/${var.google_project_id}/locations/us" + location = "us" + display_name = "c7n-dlp-discovery-configs" + status = "PAUSED" + inspect_templates = [google_data_loss_prevention_inspect_template.c7n.id] + + targets { + big_query_target { + filter { + other_tables {} + } + } + } +} diff --git a/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/tf_resources.json b/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/tf_resources.json new file mode 100644 index 00000000000..fe6ba20722a --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/project_dlp_discovery_configs/tf_resources.json @@ -0,0 +1,84 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_data_loss_prevention_discovery_config": { + "c7n": { + "actions": [], + "create_time": "2026-10-02T14:02:36.223251Z", + "deletion_policy": "DELETE", + "display_name": "c7n-dlp-discovery-configs", + "errors": [], + "id": "projects/cloud-custodian/locations/us/discoveryConfigs/8338945954561565050", + "inspect_templates": [ + "projects/cloud-custodian/locations/us/inspectTemplates/4167487095981753467" + ], + "last_run_time": "", + "location": "us", + "name": "8338945954561565050", + "org_config": [], + "other_cloud_starting_location": [], + "parent": "projects/cloud-custodian/locations/us", + "status": "PAUSED", + "targets": [ + { + "big_query_target": [ + { + "cadence": [], + "conditions": [], + "disabled": [], + "filter": [ + { + "other_tables": [ + {} + ], + "table_reference": [], + "tables": [] + } + ] + } + ], + "cloud_sql_target": [], + "cloud_storage_target": [], + "other_cloud_target": [], + "secrets_target": [] + } + ], + "timeouts": null, + "update_time": "2026-10-02T14:02:36.223251Z" + } + }, + "google_data_loss_prevention_inspect_template": { + "c7n": { + "allow_limited_availability_info_types": false, + "deletion_policy": "DELETE", + "description": "", + "display_name": "c7n-dlp-discovery-configs", + "id": "projects/cloud-custodian/locations/us/inspectTemplates/4167487095981753467", + "inspect_config": [ + { + "content_options": null, + "custom_info_types": [], + "exclude_info_types": false, + "include_quote": false, + "info_types": [ + { + "name": "EMAIL_ADDRESS", + "sensitivity_score": [], + "version": "" + } + ], + "limits": [], + "min_likelihood": "POSSIBLE", + "min_likelihood_per_info_type": [], + "rule_set": [] + } + ], + "name": "4167487095981753467", + "parent": "projects/cloud-custodian/locations/us", + "template_id": "4167487095981753467", + "timeouts": null + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_resourcemanager.py b/tools/c7n_gcp/tests/test_resourcemanager.py index 39cc1738095..4c3365a0fab 100644 --- a/tools/c7n_gcp/tests/test_resourcemanager.py +++ b/tools/c7n_gcp/tests/test_resourcemanager.py @@ -10,7 +10,7 @@ from pytest_terraform import terraform from c7n_gcp.resources.resourcemanager import ( - FolderIamPolicyFilter, HierarchyAction, OrganizationIamPolicyFilter + DlpDiscoveryConfigsFilter, FolderIamPolicyFilter, HierarchyAction, OrganizationIamPolicyFilter ) from gcp_common import BaseTest @@ -18,6 +18,15 @@ from c7n.exceptions import ResourceLimitExceeded +ORG_ID = '999999999999' +FOLDER_ID = '111111111111' +UPPER_FOLDER_ID = '222222222222' +OTHER_FOLDER_ID = '333333333333' +ORG_PARENT = {'type': 'organization', 'id': ORG_ID} +FOLDER_PARENT = {'type': 'folder', 'id': FOLDER_ID} +ANCESTRY = [ + ('project', 'p'), ('folder', FOLDER_ID), ('folder', UPPER_FOLDER_ID), ('organization', ORG_ID)] + class LimitsTest(BaseTest): def test_policy_resource_limits(self): @@ -899,3 +908,216 @@ def test_project_set_iam_policy_remove_matched(test, project_iam_policy_chained) assert sa_email not in bindings.get('roles/owner', []) assert sa_email in bindings.get('roles/viewer', []) + + +@terraform("project_dlp_discovery_configs") +def test_project_dlp_discovery_configs(test, project_dlp_discovery_configs): + # recording needs the DLP API enabled and a quota project for user credentials + discovery_config = project_dlp_discovery_configs.resources[ + 'google_data_loss_prevention_discovery_config']['c7n'] + factory = test.replay_flight_data('project-dlp-discovery-configs') + p = test.load_policy( + { + 'name': 'project-dlp-discovery-configs', + 'resource': 'gcp.project', + 'query': [{'filter': f'id:{test.project_id}'}], + 'filters': [ + {'type': 'dlp-discovery-configs', 'attrs': [{'status': 'PAUSED'}]}, + ], + }, + session_factory=factory, + ) + + resources = p.run() + + assert len(resources) == 1 + assert resources[0]['projectId'] == test.project_id + matches = resources[0]['c7n:ListItemMatches'] + assert [m['name'] for m in matches] == [discovery_config['id']] + + +@pytest.mark.parametrize('resource_type, resource, component, parent', [ + pytest.param( + 'gcp.organization', {'name': f'organizations/{ORG_ID}'}, + 'organizations.locations.discoveryConfigs', f'organizations/{ORG_ID}/locations/-', + id='organization'), + pytest.param( + 'gcp.project', {'projectId': 'cloud-custodian'}, + 'projects.locations.discoveryConfigs', 'projects/cloud-custodian/locations/-', + id='project'), +]) +def test_dlp_discovery_configs_filter_get_item_values( + test, resource_type, resource, component, parent): + session = mock.Mock() + client = session.client.return_value + client.execute_paged_query.return_value = [ + {'discoveryConfigs': [{'name': 'c1'}]}, {'discoveryConfigs': [{'name': 'c2'}]}, {}] + p = test.load_policy({ + 'name': 'dlp-discovery-configs', + 'resource': resource_type, + 'filters': [{'type': 'dlp-discovery-configs'}], + }) + dlp_filter = p.resource_manager.filters[0] + + with mock.patch('c7n_gcp.resources.resourcemanager.local_session', return_value=session): + configs = dlp_filter.get_item_values(resource) + + assert configs == [{'name': 'c1'}, {'name': 'c2'}] + session.client.assert_called_once_with('dlp', 'v2', component) + client.execute_paged_query.assert_called_once_with('list', {'parent': parent}) + + +def test_dlp_discovery_configs_filter_requires_parent(): + with pytest.raises(NotImplementedError, match="subclass responsibility"): + DlpDiscoveryConfigsFilter(data={}, manager=mock.Mock()).get_parent({}) + + +def dlp_config(name, status='RUNNING', folder=None, org=None, targets=('bigQueryTarget',)): + config = {'name': name, 'status': status, 'targets': [{t: {}} for t in targets]} + if folder: + config['orgConfig'] = {'location': {'folderId': folder}} + elif org: + config['orgConfig'] = {'location': {'organizationId': org}} + return config + + +def dlp_session(own=(), org_configs=()): + dlp_projects, dlp_orgs, crm = mock.Mock(), mock.Mock(), mock.Mock() + dlp_projects.execute_paged_query.return_value = [{'discoveryConfigs': list(own)}] + dlp_orgs.execute_paged_query.return_value = [{'discoveryConfigs': list(org_configs)}] + crm.execute_command.return_value = { + 'ancestor': [{'resourceId': {'type': t, 'id': i}} for t, i in ANCESTRY]} + clients = { + ('dlp', 'v2', 'projects.locations.discoveryConfigs'): dlp_projects, + ('dlp', 'v2', 'organizations.locations.discoveryConfigs'): dlp_orgs, + ('cloudresourcemanager', 'v1', 'projects'): crm, + } + session = mock.Mock() + session.client.side_effect = lambda *args: clients[args] + return session, dlp_orgs, crm + + +def run_dlp_inherited(test, session, projects, **data): + p = test.load_policy({ + 'name': 'dlp-inherited', + 'resource': 'gcp.project', + 'filters': [dict({'type': 'dlp-discovery-configs', 'inherited': True}, **data)], + }) + with mock.patch('c7n_gcp.resources.resourcemanager.local_session', return_value=session): + return p.resource_manager.filters[0].process(projects) + + +@pytest.mark.parametrize('parent, own, org_configs, expected', [ + pytest.param(ORG_PARENT, [dlp_config('own')], [], ['own'], id='own'), + pytest.param( + FOLDER_PARENT, [dlp_config('own')], [dlp_config('folder', folder=FOLDER_ID)], + ['own', 'folder'], id='own-does-not-block'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('folder', folder=FOLDER_ID)], ['folder'], + id='parent-folder'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('upper', folder=UPPER_FOLDER_ID)], ['upper'], + id='nested-folder'), + pytest.param(FOLDER_PARENT, [], [dlp_config('org', org=ORG_ID)], ['org'], id='org'), + pytest.param(FOLDER_PARENT, [], [], [], id='none'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('paused', 'PAUSED', folder=FOLDER_ID)], [], + id='paused'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('unset', 'STATUS_UNSPECIFIED', folder=FOLDER_ID)], [], + id='unspecified-status'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('sibling', folder=OTHER_FOLDER_ID)], [], + id='sibling-folder'), + pytest.param( + FOLDER_PARENT, [], + [dlp_config('org', org=ORG_ID), dlp_config('folder', folder=FOLDER_ID)], + ['folder'], id='closest-wins'), + pytest.param( + FOLDER_PARENT, [], + [dlp_config('upper', folder=UPPER_FOLDER_ID), dlp_config('folder', folder=FOLDER_ID)], + ['folder'], id='nearest-folder-wins'), + pytest.param( + FOLDER_PARENT, [], + [dlp_config('org', org=ORG_ID), dlp_config('paused', 'PAUSED', folder=FOLDER_ID)], + ['org'], id='paused-does-not-block'), + pytest.param( + FOLDER_PARENT, [], + [dlp_config('org', org=ORG_ID), + dlp_config('secrets', folder=FOLDER_ID, targets=('secretsTarget',))], + ['secrets', 'org'], id='other-type-does-not-block'), + pytest.param( + FOLDER_PARENT, [], [dlp_config('s3', org=ORG_ID, targets=('otherCloudTarget',))], [], + id='other-cloud-not-inherited'), + pytest.param( + None, [dlp_config('own')], [dlp_config('org', org=ORG_ID)], ['own'], + id='no-organization'), +]) +def test_dlp_discovery_configs_inherited(test, parent, own, org_configs, expected): + session, _, _ = dlp_session(own, org_configs) + project = {'projectId': 'p', 'parent': parent} if parent else {'projectId': 'p'} + + matched = run_dlp_inherited(test, session, [project]) + + assert [c['name'] for r in matched for c in r['c7n:ListItemMatches']] == expected + + +def test_dlp_discovery_configs_inherited_keeps_unclaimed_targets(test): + org = dlp_config('org', org=ORG_ID, targets=('bigQueryTarget', 'cloudSqlTarget')) + session, _, _ = dlp_session(org_configs=[org, dlp_config('folder', folder=FOLDER_ID)]) + + matched = run_dlp_inherited(test, session, [{'projectId': 'p', 'parent': FOLDER_PARENT}]) + + assert [(c['name'], c['targets']) for c in matched[0]['c7n:ListItemMatches']] == [ + ('folder', [{'bigQueryTarget': {}}]), ('org', [{'cloudSqlTarget': {}}])] + + +def test_dlp_discovery_configs_inherited_calls_and_copies(test): + session, dlp_orgs, crm = dlp_session(org_configs=[dlp_config('org', org=ORG_ID)]) + projects = [ + {'projectId': 'a', 'parent': ORG_PARENT}, + {'projectId': 'b', 'parent': FOLDER_PARENT}, + {'projectId': 'c', 'parent': FOLDER_PARENT}, + {'projectId': 'd'}, + ] + + matched = run_dlp_inherited(test, session, projects, attrs=[{'status': 'RUNNING'}]) + + assert [r['projectId'] for r in matched] == ['a', 'b', 'c'] + matches = [r['c7n:ListItemMatches'][0] for r in matched] + assert len({id(m) for m in matches}) == 3 + assert all(m['c7n:MatchedFilters'] == ['status'] for m in matches) + dlp_orgs.execute_paged_query.assert_called_once_with( + 'list', {'parent': f'organizations/{ORG_ID}/locations/-'}) + crm.execute_command.assert_called_once_with('getAncestry', {'projectId': 'b'}) + + +def test_dlp_discovery_configs_inherited_ancestry_per_folder(test): + session, _, crm = dlp_session(org_configs=[dlp_config('other', folder=OTHER_FOLDER_ID)]) + folder_of = {'a': FOLDER_ID, 'b': OTHER_FOLDER_ID} + crm.execute_command.side_effect = lambda verb, params: {'ancestor': [ + {'resourceId': {'type': 'project', 'id': params['projectId']}}, + {'resourceId': {'type': 'folder', 'id': folder_of[params['projectId']]}}, + {'resourceId': {'type': 'organization', 'id': ORG_ID}}]} + projects = [ + {'projectId': 'a', 'parent': FOLDER_PARENT}, + {'projectId': 'b', 'parent': {'type': 'folder', 'id': OTHER_FOLDER_ID}}, + ] + + matched = run_dlp_inherited(test, session, projects) + + assert [r['projectId'] for r in matched] == ['b'] + assert crm.execute_command.call_count == 2 + + +def test_dlp_discovery_configs_inherited_permissions(test): + def permissions(**data): + p = test.load_policy({ + 'name': 'dlp-permissions', + 'resource': 'gcp.project', + 'filters': [dict({'type': 'dlp-discovery-configs'}, **data)], + }) + return p.resource_manager.filters[0].get_permissions() + + assert permissions() == ('dlp.jobTriggers.list',) + assert permissions(inherited=True) == ('dlp.jobTriggers.list', 'resourcemanager.projects.get') From b2d948ffd1b569b9d720a3e85e9dd6e113ce03c7 Mon Sep 17 00:00:00 2001 From: Usman Tahir Qureshi <123512712+UsmanQT@users.noreply.github.com> Date: Thu, 8 Oct 2026 00:18:06 -0400 Subject: [PATCH 31/65] aws - invoke-lambda - fix KeyError when a qualifier is specified (#11186) The action checked self.data.get('qualifier') but then read self.data['Qualifier'], so any policy that set a qualifier (alias or version) raised KeyError: 'Qualifier' instead of invoking the function. Read the same lowercase key the schema defines. Fixes #10965 Co-authored-by: Usman Qureshi Co-authored-by: Claude Sonnet 5.5 --- c7n/actions/invoke.py | 2 +- tests/test_actions.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/c7n/actions/invoke.py b/c7n/actions/invoke.py index 944ba2dfbf0..d57e80904d1 100644 --- a/c7n/actions/invoke.py +++ b/c7n/actions/invoke.py @@ -81,7 +81,7 @@ def process(self, resources, event=None): params = dict(FunctionName=self.data['function']) if self.data.get('qualifier'): - params['Qualifier'] = self.data['Qualifier'] + params['Qualifier'] = self.data['qualifier'] if self.data.get('async', True): params['InvocationType'] = 'Event' diff --git a/tests/test_actions.py b/tests/test_actions.py index 234383281d2..80d00e73c6a 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,11 +1,43 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import io +from unittest import mock + from botocore.exceptions import ClientError from c7n.exceptions import PolicyValidationError from c7n.actions import Action, ActionRegistry from .common import BaseTest +class InvokeLambdaTest(BaseTest): + # https://github.com/cloud-custodian/cloud-custodian/issues/10965 + # a qualifier on invoke-lambda raised KeyError: 'Qualifier' + + def _invoke(self, action): + mock_factory = mock.MagicMock() + mock_factory.region = 'us-east-1' + client = mock_factory().client('lambda') + client.invoke.return_value = {'Payload': io.BytesIO(b'{}')} + p = self.load_policy( + {'name': 'invoke', 'resource': 's3', + 'actions': [dict(action, type='invoke-lambda', function='my-func')]}, + session_factory=mock_factory) + with mock.patch('c7n.utils.get_account_alias_from_sts', return_value='alias'): + p.resource_manager.actions[0].process([{'Name': 'bucket'}]) + return client.invoke + + def test_invoke_lambda_qualifier(self): + invoke = self._invoke({'qualifier': 'prod'}) + invoke.assert_called_once() + self.assertEqual(invoke.call_args.kwargs['FunctionName'], 'my-func') + self.assertEqual(invoke.call_args.kwargs['Qualifier'], 'prod') + + def test_invoke_lambda_no_qualifier(self): + invoke = self._invoke({}) + invoke.assert_called_once() + self.assertNotIn('Qualifier', invoke.call_args.kwargs) + + class ActionTest(BaseTest): def test_process_unimplemented(self): From 631a70cbb2e29e2803b1496655adfb01f22ec230 Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Thu, 8 Oct 2026 10:45:44 -0400 Subject: [PATCH 32/65] azure - disk - add snapshot action (#11190) --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- tools/c7n_azure/c7n_azure/resources/disk.py | 98 ++- .../DiskSnapshotTest.disk-snapshot-full.json | 583 ++++++++++++++++ ...kSnapshotTest.disk-snapshot-tag-limit.json | 631 ++++++++++++++++++ .../DiskSnapshotTest.disk-snapshot.json | 586 ++++++++++++++++ .../templates/disk-snapshot-full.json | 24 + .../templates/disk-snapshot-tag-limit.json | 91 +++ .../tests_azure/templates/disk-snapshot.json | 24 + .../tests_azure/tests_resources/test_disk.py | 200 +++++- 8 files changed, 2233 insertions(+), 4 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-full.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-tag-limit.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot.json create mode 100644 tools/c7n_azure/tests_azure/templates/disk-snapshot-full.json create mode 100644 tools/c7n_azure/tests_azure/templates/disk-snapshot-tag-limit.json create mode 100644 tools/c7n_azure/tests_azure/templates/disk-snapshot.json diff --git a/tools/c7n_azure/c7n_azure/resources/disk.py b/tools/c7n_azure/c7n_azure/resources/disk.py index a6fd8566ac1..fdbac97cc75 100644 --- a/tools/c7n_azure/c7n_azure/resources/disk.py +++ b/tools/c7n_azure/c7n_azure/resources/disk.py @@ -7,7 +7,8 @@ from c7n_azure.provider import resources from c7n.utils import type_schema from c7n_azure.actions.base import AzureBaseAction -from c7n_azure.utils import ResourceIdParser, ThreadHelper +from c7n_azure.filters import DEFAULT_TAG +from c7n_azure.utils import ResourceIdParser, StringUtils, ThreadHelper from azure.core.exceptions import AzureError @@ -532,3 +533,98 @@ def extract_resource_group_and_name(resource_id): resource_group = ResourceIdParser.get_resource_group(resource_id) resource_name = ResourceIdParser.get_resource_name(resource_id) return resource_group, resource_name + + +@Disk.action_registry.register('snapshot') +class DiskSnapshotAction(AzureBaseAction): + """Create a snapshot of each disk. + + Snapshots go in the disk's resource group and are named + ``-`` (UTC). Names over Azure's 80-character limit are + shortened, with a hash of the disk ID added to keep them unique. + + ``incremental`` (default ``true``) stores only the changes since the disk's previous + snapshot. Set it to ``false`` for a full copy. Azure supports only incremental + snapshots of Ultra Disks and Premium SSD v2 disks, and finishes copying them after + the action returns. + + Azure gives the snapshot the disk's encryption settings, including a customer-managed + key. The snapshot gets the disk's tags, except the ``custodian_status`` tag from + ``mark-for-op``, plus a ``custodian_snapshot`` tag set to the policy name. A snapshot + isn't taken if that comes to more than Azure's limit of 50 tags. + + If a snapshot fails, snapshots of the remaining disks are still taken, then the + action raises so later actions in the policy don't run. + + Needs at least ``Microsoft.Compute/disks/read``, ``Microsoft.Compute/snapshots/write`` + and ``Microsoft.Compute/snapshots/read``. + + :example: + + Take a snapshot of every disk tagged ``backup: daily``: + + .. code-block:: yaml + + policies: + - name: azure-disk-snapshot-daily + resource: azure.disk + filters: + - "tag:backup": daily + actions: + - type: snapshot + """ + + schema = type_schema('snapshot', incremental={'type': 'boolean'}) + + def _process_resources(self, resources, event): + # The base class logs and swallows errors outside pytest, so a later action such + # as delete would still run. Snapshot the rest, then raise. + self._prepare_processing() + failures = [] + for resource in resources: + try: + self._log_modified_resource(resource, self._process_resource(resource)) + except Exception as e: + self.log.error( + "Snapshot of disk '%s' in resource group '%s' failed: %s", + resource.get('name'), resource.get('resourceGroup'), e, + exc_info=True, extra=self._get_action_log_metadata(resource)) + failures.append(e) + if failures: + raise failures[0] + + def _prepare_processing(self): + self.client = self.manager.get_client() + + def _process_resource(self, resource): + # Imported here so the test harness's patch of utcnow takes effect. + from c7n_azure.utils import utcnow + + name = self.snapshot_name(resource['name'], resource['id'], utcnow()) + self.client.snapshots.begin_create_or_update( + resource['resourceGroup'], name, self.snapshot_body(resource)).result() + return f"Created snapshot '{name}'" + + def snapshot_body(self, disk): + """Return the request body that creates a snapshot of ``disk``.""" + tags = { + k: v for k, v in (disk.get('tags') or {}).items() + if k.lower() not in ('custodian_snapshot', DEFAULT_TAG) + } + tags['custodian_snapshot'] = self.manager.ctx.policy.name[:256] + return { + 'location': disk['location'], + 'creation_data': {'create_option': 'Copy', 'source_resource_id': disk['id']}, + 'incremental': self.data.get('incremental', True), + 'tags': tags, + } + + @staticmethod + def snapshot_name(disk_name, disk_id, now): + """Return a valid Azure snapshot name, unique per disk and second.""" + stamp = now.strftime('%Y%m%d%H%M%S') + name = f'{disk_name}-{stamp}' + if len(name) <= 80: + return name + suffix = f'-{StringUtils.naming_hash(disk_id.lower())}-{stamp}' + return disk_name[:80 - len(suffix)] + suffix diff --git a/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-full.json b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-full.json new file mode 100644 index 00000000000..9e9b5402cb3 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-full.json @@ -0,0 +1,583 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/disks?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:53 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;236,Microsoft.Compute/HighCostGet30Min;1916" + ], + "x-msedge-ref": [ + "Ref A: 94D4E800D31641ACBD71E13EA4BDE9D4 Ref B: CHI261140508062 Ref C: 2026-10-07T15:05:53Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "3804" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "9c1b6303-d7ae-421f-bdef-e4fff83ae304" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-full-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.0957295+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + } + }, + { + "name": "cctest-snapshot-limit-a", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-a", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "tag01": "value", + "tag02": "value", + "tag03": "value", + "tag04": "value", + "tag05": "value", + "tag06": "value", + "tag07": "value", + "tag08": "value", + "tag09": "value", + "tag10": "value", + "tag11": "value", + "tag12": "value", + "tag13": "value", + "tag14": "value", + "tag15": "value", + "tag16": "value", + "tag17": "value", + "tag18": "value", + "tag19": "value", + "tag20": "value", + "tag21": "value", + "tag22": "value", + "tag23": "value", + "tag24": "value", + "tag25": "value", + "tag26": "value", + "tag27": "value", + "tag28": "value", + "tag29": "value", + "tag30": "value", + "tag31": "value", + "tag32": "value", + "tag33": "value", + "tag34": "value", + "tag35": "value", + "tag36": "value", + "tag37": "value", + "tag38": "value", + "tag39": "value", + "tag40": "value", + "tag41": "value", + "tag42": "value", + "tag43": "value", + "tag44": "value", + "tag45": "value", + "tag46": "value", + "tag47": "value", + "tag48": "value", + "tag49": "value", + "tag50": "value" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "4acbaeb5-a32e-4ce3-bc6f-a67bd6bf7982" + } + }, + { + "name": "cctest-snapshot-limit-b", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + } + }, + { + "name": "cctest-snapshot-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.7588432+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + } + } + ] + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959?api-version=2024-03-02", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:53 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-msedge-ref": [ + "Ref A: 760FE1A1727A4AD795AD36F663B84D97 Ref B: CHI261140504042 Ref C: 2026-10-07T15:05:54Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/151d076e-15a5-4f12-a149-3356e0c0b3d5?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823547267119&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostSnapshotCreateHydrate3Min;998,Microsoft.Compute/HighCostSnapshotCreateHydrate30Min;7998" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/151d076e-15a5-4f12-a149-3356e0c0b3d5?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&monitor=true&api-version=2024-03-02&t=639269823547267119&c=redacted&s=redacted&h=redacted" + ], + "content-length": [ + "748" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-full-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk" + }, + "incremental": false, + "provisioningState": "Updating" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/151d076e-15a5-4f12-a149-3356e0c0b3d5?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823547267119&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:54 GMT" + ], + "x-msedge-ref": [ + "Ref A: 4ABB3AEE576B47A09098C9B3E0FBF45A Ref B: CHI261140514060 Ref C: 2026-10-07T15:05:54Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49984,Microsoft.Compute/GetOperation30Min;399984" + ], + "content-length": [ + "134" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:05:54.6590984+00:00", + "status": "InProgress", + "name": "151d076e-15a5-4f12-a149-3356e0c0b3d5" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/151d076e-15a5-4f12-a149-3356e0c0b3d5?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823547267119&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:04 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49982,Microsoft.Compute/GetOperation30Min;399982" + ], + "x-msedge-ref": [ + "Ref A: 4B77CEAE5D6D48908DB1773CD0E8F0BE Ref B: CHI261140507031 Ref C: 2026-10-07T15:06:05Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1267" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:05:54.6590984+00:00", + "endTime": "2026-10-07T15:05:55.8841132+00:00", + "status": "Succeeded", + "properties": { + "output": { + "name": "cctest-snapshot-full-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "sourceUniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": false, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:54.6638236+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "8553493c-1c9b-4751-8f94-75f2ec0e9bf0" + } + } + }, + "name": "151d076e-15a5-4f12-a149-3356e0c0b3d5" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:05 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGet3Min;14986,Microsoft.Compute/LowCostGet30Min;119986" + ], + "x-msedge-ref": [ + "Ref A: E6F6F771342E448FB028A89216D331F5 Ref B: CHI261140512036 Ref C: 2026-10-07T15:06:05Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1244" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-full-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "sourceUniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": false, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:54.6638236+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "8553493c-1c9b-4751-8f94-75f2ec0e9bf0" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot-full/providers/Microsoft.Compute/snapshots?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:06 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;235,Microsoft.Compute/HighCostGet30Min;1915" + ], + "x-msedge-ref": [ + "Ref A: 9BBF03122CEA4EEE8203177C69040FCB Ref B: CHI261140512025 Ref C: 2026-10-07T15:06:06Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1054" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "32d65a0f-441e-49d9-be28-7a7ed5f93705" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-full-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot-full/providers/Microsoft.Compute/snapshots/cctest-snapshot-full-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "sourceUniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": false, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:54.6638236+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "8553493c-1c9b-4751-8f94-75f2ec0e9bf0" + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-tag-limit.json b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-tag-limit.json new file mode 100644 index 00000000000..dfad5038d0c --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot-tag-limit.json @@ -0,0 +1,631 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/disks?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:07 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;234,Microsoft.Compute/HighCostGet30Min;1914" + ], + "x-msedge-ref": [ + "Ref A: A0DD4825DDE84FD0AFAF4CA874CF5D7F Ref B: CHI261140508023 Ref C: 2026-10-07T15:06:07Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "3804" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "173b1b79-590c-447d-9d94-ab72f73357df" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-full-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.0957295+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + } + }, + { + "name": "cctest-snapshot-limit-a", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-a", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "tag01": "value", + "tag02": "value", + "tag03": "value", + "tag04": "value", + "tag05": "value", + "tag06": "value", + "tag07": "value", + "tag08": "value", + "tag09": "value", + "tag10": "value", + "tag11": "value", + "tag12": "value", + "tag13": "value", + "tag14": "value", + "tag15": "value", + "tag16": "value", + "tag17": "value", + "tag18": "value", + "tag19": "value", + "tag20": "value", + "tag21": "value", + "tag22": "value", + "tag23": "value", + "tag24": "value", + "tag25": "value", + "tag26": "value", + "tag27": "value", + "tag28": "value", + "tag29": "value", + "tag30": "value", + "tag31": "value", + "tag32": "value", + "tag33": "value", + "tag34": "value", + "tag35": "value", + "tag36": "value", + "tag37": "value", + "tag38": "value", + "tag39": "value", + "tag40": "value", + "tag41": "value", + "tag42": "value", + "tag43": "value", + "tag44": "value", + "tag45": "value", + "tag46": "value", + "tag47": "value", + "tag48": "value", + "tag49": "value", + "tag50": "value" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "4acbaeb5-a32e-4ce3-bc6f-a67bd6bf7982" + } + }, + { + "name": "cctest-snapshot-limit-b", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + } + }, + { + "name": "cctest-snapshot-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.7588432+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + } + } + ] + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-a-20261007235959?api-version=2024-03-02", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 400, + "message": "Bad Request" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:07 GMT" + ], + "x-ms-failure-cause": [ + "gateway" + ], + "x-msedge-ref": [ + "Ref A: D186F6DAE3D740C8AB0B99F3E9618631 Ref B: CHI261140504025 Ref C: 2026-10-07T15:06:08Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "137" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "error": { + "code": "InvalidTags", + "message": "Too many tags specified. Requested tag count - '51'. Maximum number of tags allowed - '50'. " + } + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959?api-version=2024-03-02", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:09 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-msedge-ref": [ + "Ref A: 3DF197E5ECB1472E93C2A2ED8946B513 Ref B: CHI261140512060 Ref C: 2026-10-07T15:06:08Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/17e0539a-92fa-4521-bacd-470bcab8e7ea?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823694881143&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostSnapshotCreateHydrate3Min;997,Microsoft.Compute/HighCostSnapshotCreateHydrate30Min;7997" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/17e0539a-92fa-4521-bacd-470bcab8e7ea?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&monitor=true&api-version=2024-03-02&t=639269823694881143&c=redacted&s=redacted&h=redacted" + ], + "content-length": [ + "751" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-limit-b-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b" + }, + "incremental": true, + "provisioningState": "Updating" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/17e0539a-92fa-4521-bacd-470bcab8e7ea?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823694881143&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:09 GMT" + ], + "x-msedge-ref": [ + "Ref A: EB0CE48A851C421FB34860FF894F1EA7 Ref B: CHI261140507036 Ref C: 2026-10-07T15:06:09Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49981,Microsoft.Compute/GetOperation30Min;399981" + ], + "content-length": [ + "134" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:06:09.3147984+00:00", + "status": "InProgress", + "name": "17e0539a-92fa-4521-bacd-470bcab8e7ea" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/17e0539a-92fa-4521-bacd-470bcab8e7ea?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823694881143&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:20 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49979,Microsoft.Compute/GetOperation30Min;399979" + ], + "x-msedge-ref": [ + "Ref A: DBD35C31D1A3459FBA3B9D6496FA419A Ref B: CHI261140511054 Ref C: 2026-10-07T15:06:20Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1339" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:06:09.3147984+00:00", + "endTime": "2026-10-07T15:06:10.2180729+00:00", + "status": "Succeeded", + "properties": { + "output": { + "name": "cctest-snapshot-limit-b-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "sourceUniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:06:09.3230991+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "a2d1ca6e-4aa8-4f84-9fa2-1533c323a922", + "incrementalSnapshotFamilyId": "2259d8cd-f3a1-4eaf-afdf-b3dc94486edf" + } + } + }, + "name": "17e0539a-92fa-4521-bacd-470bcab8e7ea" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:21 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGet3Min;14983,Microsoft.Compute/LowCostGet30Min;119983" + ], + "x-msedge-ref": [ + "Ref A: 299BD276CA684B88AB309C3D6048D675 Ref B: CHI261140504042 Ref C: 2026-10-07T15:06:20Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1323" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-limit-b-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "sourceUniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:06:09.3230991+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "a2d1ca6e-4aa8-4f84-9fa2-1533c323a922", + "incrementalSnapshotFamilyId": "2259d8cd-f3a1-4eaf-afdf-b3dc94486edf" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot-tag-limit/providers/Microsoft.Compute/snapshots?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:06:22 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;233,Microsoft.Compute/HighCostGet30Min;1913" + ], + "x-msedge-ref": [ + "Ref A: 7EE96539D79C416B9F220BF511CFCB3F Ref B: CHI261140503031 Ref C: 2026-10-07T15:06:22Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1126" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "babe8a27-3eda-428e-bc61-9ae7adce3203" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-limit-b-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot-tag-limit/providers/Microsoft.Compute/snapshots/cctest-snapshot-limit-b-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "sourceUniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:06:09.3230991+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "a2d1ca6e-4aa8-4f84-9fa2-1533c323a922", + "incrementalSnapshotFamilyId": "2259d8cd-f3a1-4eaf-afdf-b3dc94486edf" + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot.json b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot.json new file mode 100644 index 00000000000..f494a082b70 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/DiskSnapshotTest.disk-snapshot.json @@ -0,0 +1,586 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/disks?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:38 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;238,Microsoft.Compute/HighCostGet30Min;1918" + ], + "x-msedge-ref": [ + "Ref A: FBB00D5C993E4D1EA3CE6BC113D0765E Ref B: CHI261140502023 Ref C: 2026-10-07T15:05:39Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "3804" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "92430c06-7b81-4807-b1ac-b34e6541bede" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-full-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-FULL/providers/Microsoft.Compute/disks/cctest-snapshot-full-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.0957295+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "57c2a59e-4f59-4bc9-a040-0ad6976d8120" + } + }, + { + "name": "cctest-snapshot-limit-a", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-a", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "tag01": "value", + "tag02": "value", + "tag03": "value", + "tag04": "value", + "tag05": "value", + "tag06": "value", + "tag07": "value", + "tag08": "value", + "tag09": "value", + "tag10": "value", + "tag11": "value", + "tag12": "value", + "tag13": "value", + "tag14": "value", + "tag15": "value", + "tag16": "value", + "tag17": "value", + "tag18": "value", + "tag19": "value", + "tag20": "value", + "tag21": "value", + "tag22": "value", + "tag23": "value", + "tag24": "value", + "tag25": "value", + "tag26": "value", + "tag27": "value", + "tag28": "value", + "tag29": "value", + "tag30": "value", + "tag31": "value", + "tag32": "value", + "tag33": "value", + "tag34": "value", + "tag35": "value", + "tag36": "value", + "tag37": "value", + "tag38": "value", + "tag39": "value", + "tag40": "value", + "tag41": "value", + "tag42": "value", + "tag43": "value", + "tag44": "value", + "tag45": "value", + "tag46": "value", + "tag47": "value", + "tag48": "value", + "tag49": "value", + "tag50": "value" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "4acbaeb5-a32e-4ce3-bc6f-a67bd6bf7982" + } + }, + { + "name": "cctest-snapshot-limit-b", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT-TAG-LIMIT/providers/Microsoft.Compute/disks/cctest-snapshot-limit-b", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:51.0352411+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "f6b4697f-bf79-4fcf-b989-0aab7e21c455" + } + }, + { + "name": "cctest-snapshot-disk", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "type": "Microsoft.Compute/disks", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4, + "diskIOPSReadWrite": 500, + "diskMBpsReadWrite": 60, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:04:50.7588432+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + } + } + ] + } + } + } + }, + { + "request": { + "method": "PUT", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959?api-version=2024-03-02", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:40 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-msedge-ref": [ + "Ref A: 690212B55E4843C797933578526D6F68 Ref B: CHI261140512023 Ref C: 2026-10-07T15:05:39Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/7316cc6e-e62d-4d50-8701-f2e89b133fc7?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823405507731&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostSnapshotCreateHydrate3Min;999,Microsoft.Compute/HighCostSnapshotCreateHydrate30Min;7999" + ], + "location": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/7316cc6e-e62d-4d50-8701-f2e89b133fc7?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&monitor=true&api-version=2024-03-02&t=639269823405507731&c=redacted&s=redacted&h=redacted" + ], + "content-length": [ + "722" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk" + }, + "incremental": true, + "provisioningState": "Updating" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/7316cc6e-e62d-4d50-8701-f2e89b133fc7?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823405507731&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:40 GMT" + ], + "x-msedge-ref": [ + "Ref A: E2A8A9706C3D423DABE2F457FB721B78 Ref B: CHI261140513054 Ref C: 2026-10-07T15:05:40Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49987,Microsoft.Compute/GetOperation30Min;399987" + ], + "content-length": [ + "134" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:05:40.4027572+00:00", + "status": "InProgress", + "name": "7316cc6e-e62d-4d50-8701-f2e89b133fc7" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/DiskOperations/7316cc6e-e62d-4d50-8701-f2e89b133fc7?p=98acd7d6-03f6-45a6-b0f1-6a08d463d7b7&api-version=2024-03-02&t=639269823405507731&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:50 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperation3Min;49985,Microsoft.Compute/GetOperation30Min;399985" + ], + "x-msedge-ref": [ + "Ref A: E0D8B4B6BF984D16960CB1C73ACE2800 Ref B: CHI261140513054 Ref C: 2026-10-07T15:05:51Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1310" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "startTime": "2026-10-07T15:05:40.4027572+00:00", + "endTime": "2026-10-07T15:05:41.8519989+00:00", + "status": "Succeeded", + "properties": { + "output": { + "name": "cctest-snapshot-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "sourceUniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:40.4131749+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "9b3bae82-97ea-4d49-b840-81217ed660bd", + "incrementalSnapshotFamilyId": "01e7ebae-9392-4e2c-86a7-9a4c9ed59c0f" + } + } + }, + "name": "7316cc6e-e62d-4d50-8701-f2e89b133fc7" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:51 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGet3Min;14989,Microsoft.Compute/LowCostGet30Min;119989" + ], + "x-msedge-ref": [ + "Ref A: 7872F1FB87A64A988285536B42795B29 Ref B: CHI261140504060 Ref C: 2026-10-07T15:05:51Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1294" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "name": "cctest-snapshot-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "sourceUniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:40.4131749+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "9b3bae82-97ea-4d49-b840-81217ed660bd", + "incrementalSnapshotFamilyId": "01e7ebae-9392-4e2c-86a7-9a4c9ed59c0f" + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot/providers/Microsoft.Compute/snapshots?api-version=2024-03-02", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Wed, 07 Oct 2026 15:05:52 GMT" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGet3Min;237,Microsoft.Compute/HighCostGet30Min;1917" + ], + "x-msedge-ref": [ + "Ref A: 94C8957FBD344C33BE82B4350452B720 Ref B: CHI261140501052 Ref C: 2026-10-07T15:05:52Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "1097" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "aaa4fa02-4a3d-40ef-bdfa-47b054e4bb15" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctest-snapshot-disk-20261007235959", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_disk-snapshot/providers/Microsoft.Compute/snapshots/cctest-snapshot-disk-20261007235959", + "type": "Microsoft.Compute/snapshots", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "custodian_snapshot": "test-azure-disk-snapshot" + }, + "sku": { + "name": "Standard_ZRS", + "tier": "Standard" + }, + "properties": { + "creationData": { + "createOption": "Copy", + "sourceResourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_DISK-SNAPSHOT/providers/Microsoft.Compute/disks/cctest-snapshot-disk", + "sourceUniqueId": "08470427-9f14-4b07-834d-a7eed1c1dc39" + }, + "diskSizeGB": 4, + "encryption": { + "type": "EncryptionAtRestWithPlatformKey" + }, + "incremental": true, + "networkAccessPolicy": "AllowAll", + "publicNetworkAccess": "Enabled", + "timeCreated": "2026-10-07T15:05:40.4131749+00:00", + "provisioningState": "Succeeded", + "diskState": "Unattached", + "diskSizeBytes": 4294967296, + "uniqueId": "9b3bae82-97ea-4d49-b840-81217ed660bd", + "incrementalSnapshotFamilyId": "01e7ebae-9392-4e2c-86a7-9a4c9ed59c0f" + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/disk-snapshot-full.json b/tools/c7n_azure/tests_azure/templates/disk-snapshot-full.json new file mode 100644 index 00000000000..a60bcaeba6a --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/disk-snapshot-full.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "cctest-snapshot-full-disk", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "tags": { + "testtag": "testvalue" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4 + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/templates/disk-snapshot-tag-limit.json b/tools/c7n_azure/tests_azure/templates/disk-snapshot-tag-limit.json new file mode 100644 index 00000000000..654a3c1e220 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/disk-snapshot-tag-limit.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "cctest-snapshot-limit-a", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "tags": { + "tag01": "value", + "tag02": "value", + "tag03": "value", + "tag04": "value", + "tag05": "value", + "tag06": "value", + "tag07": "value", + "tag08": "value", + "tag09": "value", + "tag10": "value", + "tag11": "value", + "tag12": "value", + "tag13": "value", + "tag14": "value", + "tag15": "value", + "tag16": "value", + "tag17": "value", + "tag18": "value", + "tag19": "value", + "tag20": "value", + "tag21": "value", + "tag22": "value", + "tag23": "value", + "tag24": "value", + "tag25": "value", + "tag26": "value", + "tag27": "value", + "tag28": "value", + "tag29": "value", + "tag30": "value", + "tag31": "value", + "tag32": "value", + "tag33": "value", + "tag34": "value", + "tag35": "value", + "tag36": "value", + "tag37": "value", + "tag38": "value", + "tag39": "value", + "tag40": "value", + "tag41": "value", + "tag42": "value", + "tag43": "value", + "tag44": "value", + "tag45": "value", + "tag46": "value", + "tag47": "value", + "tag48": "value", + "tag49": "value", + "tag50": "value" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4 + } + }, + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "cctest-snapshot-limit-b", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "tags": { + "testtag": "testvalue" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4 + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/templates/disk-snapshot.json b/tools/c7n_azure/tests_azure/templates/disk-snapshot.json new file mode 100644 index 00000000000..baaedbd4b7c --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/disk-snapshot.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Compute/disks", + "apiVersion": "2024-03-02", + "name": "cctest-snapshot-disk", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "tags": { + "testtag": "testvalue" + }, + "properties": { + "creationData": { + "createOption": "Empty" + }, + "diskSizeGB": 4 + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_disk.py b/tools/c7n_azure/tests_azure/tests_resources/test_disk.py index 274871e3f36..6c3c569c3ae 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_disk.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_disk.py @@ -1,14 +1,19 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import datetime import logging +from c7n.exceptions import PolicyValidationError from c7n.utils import local_session from c7n_azure.session import Session from c7n_azure.utils import ResourceIdParser, ThreadHelper from unittest.mock import patch, MagicMock import pytest -from ..azure_common import BaseTest, arm_template, cassette_name -from azure.core.exceptions import AzureError -from c7n_azure.resources.disk import ModifyDiskTypeAction +from ..azure_common import ( + DEFAULT_SUBSCRIPTION_ID, BaseTest, arm_template, cassette_name, requires_arm_polling, + strict_cassette +) +from azure.core.exceptions import AzureError, HttpResponseError +from c7n_azure.resources.disk import DiskSnapshotAction, ModifyDiskTypeAction from azure.mgmt.compute import ComputeManagementClient @@ -559,3 +564,192 @@ def test_process_resources_skipped_disks(self): self.assertIn("Skipping disk 'disk1'", cm.output[0]) self.assertIn("Skipping disk 'disk2'", cm.output[1]) + + +# Polling stays on in playback so the recorded poll requests are replayed, which +# @strict_cassette requires. +@requires_arm_polling +class DiskSnapshotTest(BaseTest): + + now = datetime.datetime(2026, 1, 2, 3, 4, 5) + + @staticmethod + def _disk_id(disk_name): + return ( + f'/subscriptions/{DEFAULT_SUBSCRIPTION_ID}/resourceGroups/test_disk' + f'/providers/Microsoft.Compute/disks/{disk_name}' + ) + + def _snapshot_name_for(self, disk_name): + return DiskSnapshotAction.snapshot_name(disk_name, self._disk_id(disk_name), self.now) + + def _load_snapshot_policy(self, name='test-azure-disk-snapshot', **options): + with self.sign_out_patch(): + return self.load_policy({ + 'name': name, + 'resource': 'azure.disk', + 'actions': [{'type': 'snapshot', **options}], + }, validate=True) + + def _snapshot_body_for(self, tags, name='test-azure-disk-snapshot', **options): + action = self._load_snapshot_policy(name, **options).resource_manager.actions[0] + disk = { + 'name': 'cctest-snapshot-disk', + 'id': self._disk_id('cctest-snapshot-disk'), + 'location': 'southcentralus', + 'tags': tags, + } + return action.snapshot_body(disk) + + def _snapshot_policy(self, name_pattern, **options): + return self.load_policy({ + 'name': 'test-azure-disk-snapshot', + 'resource': 'azure.disk', + 'filters': [{'type': 'value', 'key': 'name', 'op': 'regex', 'value': name_pattern}], + 'actions': [{'type': 'snapshot', **options}], + }, validate=True) + + def _run_snapshot_policy(self, name_pattern, **options): + return self._snapshot_policy(name_pattern, **options).run() + + def _snapshots_in(self, resource_group): + client = self.session.client('azure.mgmt.compute.ComputeManagementClient') + return list(client.snapshots.list_by_resource_group(resource_group)) + + def test_snapshot_schema_registers_the_action(self): + p = self._load_snapshot_policy() + + assert isinstance(p.resource_manager.actions[0], DiskSnapshotAction) + + def test_snapshot_schema_accepts_boolean_incremental(self): + p = self._load_snapshot_policy(incremental=False) + + assert p.resource_manager.actions[0].data['incremental'] is False + + def test_snapshot_schema_rejects_misspelled_option(self): + with pytest.raises(PolicyValidationError, match="'incremntal' was unexpected"): + self._load_snapshot_policy(incremntal=False) + + def test_snapshot_schema_rejects_non_boolean_incremental(self): + with pytest.raises(PolicyValidationError, match="'yes' is not of type 'boolean'"): + self._load_snapshot_policy(incremental='yes') + + def test_snapshot_body_copies_the_disk_and_defaults_to_incremental(self): + disk_tags = {'testtag': 'testvalue'} + + body = self._snapshot_body_for(disk_tags) + + assert body == { + 'location': 'southcentralus', + 'creation_data': { + 'create_option': 'Copy', + 'source_resource_id': self._disk_id('cctest-snapshot-disk'), + }, + 'incremental': True, + 'tags': {'testtag': 'testvalue', 'custodian_snapshot': 'test-azure-disk-snapshot'}, + } + assert disk_tags == {'testtag': 'testvalue'} + + def test_snapshot_body_full_copy_when_incremental_is_false(self): + assert self._snapshot_body_for({}, incremental=False)['incremental'] is False + + def test_snapshot_body_replaces_a_marker_tag_in_any_case(self): + body = self._snapshot_body_for({'Custodian_Snapshot': 'old'}) + + assert body['tags'] == {'custodian_snapshot': 'test-azure-disk-snapshot'} + + def test_snapshot_body_skips_the_mark_for_op_tag(self): + body = self._snapshot_body_for({'custodian_status': 'delete@2026/11/01', 'env': 'dev'}) + + assert body['tags'] == {'env': 'dev', 'custodian_snapshot': 'test-azure-disk-snapshot'} + + def test_snapshot_body_caps_the_marker_at_256_characters(self): + body = self._snapshot_body_for(None, name='p' * 300) + + assert body['tags'] == {'custodian_snapshot': 'p' * 256} + + def test_snapshot_failure_of_any_kind_does_not_stop_other_disks(self): + action = self._load_snapshot_policy().resource_manager.actions[0] + disks_without_ids = [{'name': 'cctest-no-id-1'}, {'name': 'cctest-no-id-2'}] + + with self.assertLogs('custodian.azure.AzureBaseAction', level='ERROR') as logs: + with pytest.raises(KeyError): + action.process(disks_without_ids) + + assert any("'cctest-no-id-1'" in line for line in logs.output) + assert any("'cctest-no-id-2'" in line for line in logs.output) + + def test_snapshot_name_appends_timestamp_to_short_disk_name(self): + name = self._snapshot_name_for('cctestvm_OsDisk_1_81338ced63fa4855b8a5f3e2bab5213c') + + assert name == 'cctestvm_OsDisk_1_81338ced63fa4855b8a5f3e2bab5213c-20260102030405' + + def test_snapshot_name_keeps_whole_disk_name_at_80_characters(self): + disk_name = 'a' * 65 + + assert self._snapshot_name_for(disk_name) == f'{disk_name}-20260102030405' + + def test_snapshot_name_truncates_and_hashes_past_80_characters(self): + name = self._snapshot_name_for('a' * 66) + + assert name == 'a' * 56 + '-af248e7c-20260102030405' + + def test_snapshot_name_keeps_truncated_names_unique(self): + first = self._snapshot_name_for('a' * 65 + '1') + second = self._snapshot_name_for('a' * 65 + '2') + + assert first[:56] == second[:56] + assert first != second + + def test_snapshot_name_ignores_disk_id_capitalization(self): + disk_name = 'a' * 66 + disk_id = self._disk_id(disk_name) + + lower = DiskSnapshotAction.snapshot_name(disk_name, disk_id, self.now) + upper = DiskSnapshotAction.snapshot_name(disk_name, disk_id.upper(), self.now) + + assert lower == upper + + @arm_template('disk-snapshot.json') + @strict_cassette('disk-snapshot') + def test_snapshot_creates_tagged_incremental_snapshot(self): + disks = self._run_snapshot_policy('^cctest-snapshot-disk$') + + assert [d['name'] for d in disks] == ['cctest-snapshot-disk'] + snapshots = self._snapshots_in('test_disk-snapshot') + assert len(snapshots) == 1 + snapshot = snapshots[0] + assert snapshot.creation_data.source_resource_id.lower() == disks[0]['id'].lower() + assert snapshot.creation_data.create_option == 'Copy' + assert snapshot.incremental is True + assert snapshot.tags == { + 'testtag': 'testvalue', + 'custodian_snapshot': 'test-azure-disk-snapshot', + } + assert snapshot.provisioning_state == 'Succeeded' + + @arm_template('disk-snapshot-full.json') + @strict_cassette('disk-snapshot-full') + def test_snapshot_full_copy_when_incremental_is_false(self): + disks = self._run_snapshot_policy('^cctest-snapshot-full-disk$', incremental=False) + + assert [d['name'] for d in disks] == ['cctest-snapshot-full-disk'] + snapshots = self._snapshots_in('test_disk-snapshot-full') + assert len(snapshots) == 1 + assert snapshots[0].creation_data.source_resource_id.lower() == disks[0]['id'].lower() + assert snapshots[0].incremental is False + + @arm_template('disk-snapshot-tag-limit.json') + @strict_cassette('disk-snapshot-tag-limit') + def test_snapshot_failure_raises_after_snapshotting_other_disks(self): + p = self._snapshot_policy('^cctest-snapshot-limit-') + disks = sorted(p.resource_manager.resources(), key=lambda d: d['name']) + # The failing disk must come first, or this can't tell "keep going" from "stop". + assert [d['name'] for d in disks] == ['cctest-snapshot-limit-a', 'cctest-snapshot-limit-b'] + + with pytest.raises(HttpResponseError, match='Too many tags specified'): + p.resource_manager.actions[0].process(disks) + + snapshots = self._snapshots_in('test_disk-snapshot-tag-limit') + sources = [s.creation_data.source_resource_id.rsplit('/', 1)[-1] for s in snapshots] + assert sources == ['cctest-snapshot-limit-b'] From 617ff1a8cbd9527b09498a992c4ab6d1de8318d2 Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:48:38 -0500 Subject: [PATCH 33/65] gcp - bucket - add set public access action (#11189) Co-authored-by: Anthony Hillairet --- tools/c7n_gcp/c7n_gcp/resources/storage.py | 51 +++++++++++++++++ ...-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json | 50 ++++++++++++++++ ...storage.googleapis.com-storage-v1-b_1.json | 54 ++++++++++++++++++ ...-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json | 29 ++++++++++ ...m-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json | 50 ++++++++++++++++ ...storage.googleapis.com-storage-v1-b_1.json | 54 ++++++++++++++++++ ...m-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json | 29 ++++++++++ .../bucket_public_access_enforced/main.tf | 9 +++ .../tf_resources.json | 57 +++++++++++++++++++ .../bucket_public_access_inherited/main.tf | 9 +++ .../tf_resources.json | 57 +++++++++++++++++++ tools/c7n_gcp/tests/test_gcp_storage.py | 49 ++++++++++++++++ 12 files changed, 498 insertions(+) create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/patch-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/patch-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json create mode 100644 tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/tf_resources.json create mode 100644 tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/storage.py b/tools/c7n_gcp/c7n_gcp/resources/storage.py index 2ba9c2d763c..4e62fe382ab 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/storage.py +++ b/tools/c7n_gcp/c7n_gcp/resources/storage.py @@ -99,6 +99,57 @@ def get_resource_params(self, model, resource): 'body': {'iamConfiguration': {'uniformBucketLevelAccess': {'enabled': enabled}}}} +@Bucket.action_registry.register('set-public-access-prevention') +class BucketSetPublicAccess(MethodAction): + '''Set public access prevention on a bucket. + + ``state`` is required and accepts: + + - ``enforced``: requests authorized through ``allUsers`` or + ``allAuthenticatedUsers`` are rejected. Existing IAM policies and ACLs + granting that access are left in place but overridden. To remove them, + also use the ``set-iam-policy`` action with ``remove-bindings``. + - ``inherited``: public access prevention is determined by the + ``storage.publicAccessPrevention`` organization policy constraint. If + that constraint is not enforced, any existing public IAM policies or + ACLs on the bucket take effect again. + + https://cloud.google.com/storage/docs/public-access-prevention + + Example Policy: + + .. code-block:: yaml + + policies: + - name: enforce-bucket-public-access-prevention + resource: gcp.bucket + filters: + - type: value + key: iamConfiguration.publicAccessPrevention + value: enforced + op: ne + actions: + - type: set-public-access-prevention + state: enforced + ''' + + schema = type_schema( + 'set-public-access-prevention', + required=['state'], + state={'type': 'string', 'enum': ['enforced', 'inherited']}) + method_spec = {'op': 'patch'} + # buckets.patch requires setIamPolicy in addition to update when + # changing public access prevention. + # https://cloud.google.com/storage/docs/json_api/v1/buckets/patch + permissions = ('storage.buckets.update', 'storage.buckets.setIamPolicy') + + def get_resource_params(self, model, resource): + return {'bucket': resource['name'], + 'fields': 'iamConfiguration', + 'projection': 'noAcl', + 'body': {'iamConfiguration': {'publicAccessPrevention': self.data['state']}}} + + @Bucket.action_registry.register('set-iam-policy') class BucketSetIamPolicy(SetIamPolicy): """Manage GCP bucket IAM policy bindings. diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json new file mode 100644 index 00000000000..0410f870f55 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json @@ -0,0 +1,50 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "etag": "CAI=", + "date": "Wed, 07 Oct 2026 14:06:16 GMT", + "vary": "Origin, X-Origin", + "cache-control": "private, max-age=0, must-revalidate, no-transform", + "expires": "Wed, 07 Oct 2026 14:06:16 GMT", + "x-goog-gcs-base-ts": "1a116afb558", + "content-length": "947", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200", + "content-location": "https://storage.googleapis.com/storage/v1/b/c7n-pap-test-k3v9xq2m?alt=json" + }, + "body": { + "kind": "storage#bucket", + "selfLink": "https://www.googleapis.com/storage/v1/b/c7n-pap-test-k3v9xq2m", + "id": "c7n-pap-test-k3v9xq2m", + "name": "c7n-pap-test-k3v9xq2m", + "projectNumber": "000000000000", + "generation": "1791381973848252130", + "metageneration": "2", + "location": "US", + "storageClass": "STANDARD", + "etag": "CAI=", + "timeCreated": "2026-10-07T14:06:14.192Z", + "updated": "2026-10-07T14:06:16.293Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "softDeletePolicy": { + "retentionDurationSeconds": "604800", + "effectiveTime": "2026-10-07T14:06:14.192Z" + }, + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "publicAccessPrevention": "enforced" + }, + "locationType": "multi-region", + "rpo": "DEFAULT" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b_1.json new file mode 100644 index 00000000000..80709fbe9a2 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/get-storage.googleapis.com-storage-v1-b_1.json @@ -0,0 +1,54 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "date": "Wed, 07 Oct 2026 14:06:15 GMT", + "vary": "Origin, X-Origin", + "cache-control": "private, max-age=0, must-revalidate, no-transform", + "expires": "Wed, 07 Oct 2026 14:06:15 GMT", + "x-goog-gcs-base-ts": "1a116afb3a8", + "content-length": "5694", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200", + "content-location": "https://storage.googleapis.com/storage/v1/b?project=cloud-custodian&projection=full&alt=json" + }, + "body": { + "kind": "storage#buckets", + "items": [ + { + "kind": "storage#bucket", + "selfLink": "https://www.googleapis.com/storage/v1/b/c7n-pap-test-k3v9xq2m", + "id": "c7n-pap-test-k3v9xq2m", + "name": "c7n-pap-test-k3v9xq2m", + "projectNumber": "000000000000", + "generation": "1791381973848252130", + "metageneration": "1", + "location": "US", + "storageClass": "STANDARD", + "etag": "CAE=", + "timeCreated": "2026-10-07T14:06:14.192Z", + "updated": "2026-10-07T14:06:14.192Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "softDeletePolicy": { + "retentionDurationSeconds": "604800", + "effectiveTime": "2026-10-07T14:06:14.192Z" + }, + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "publicAccessPrevention": "inherited" + }, + "locationType": "multi-region", + "rpo": "DEFAULT" + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/patch-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/patch-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json new file mode 100644 index 00000000000..88cbd62b79d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-enforced/patch-storage.googleapis.com-storage-v1-b-c7n-pap-test-k3v9xq2m_1.json @@ -0,0 +1,29 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "etag": "CAI=", + "date": "Wed, 07 Oct 2026 14:06:16 GMT", + "vary": "Origin, X-Origin", + "cache-control": "no-cache, no-store, max-age=0, must-revalidate", + "expires": "Mon, 01 Jan 1990 00:00:00 GMT", + "pragma": "no-cache", + "x-goog-gcs-base-ts": "1a116afb4fd", + "content-length": "287", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200" + }, + "body": { + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:06:14.192Z" + }, + "publicAccessPrevention": "enforced" + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json new file mode 100644 index 00000000000..2096defe0cb --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json @@ -0,0 +1,50 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "etag": "CAI=", + "date": "Wed, 07 Oct 2026 14:22:40 GMT", + "vary": "Origin, X-Origin", + "cache-control": "private, max-age=0, must-revalidate, no-transform", + "expires": "Wed, 07 Oct 2026 14:22:40 GMT", + "x-goog-gcs-base-ts": "1a116beb9f2", + "content-length": "945", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200", + "content-location": "https://storage.googleapis.com/storage/v1/b/c7n-pap-inh-r7t2wd5n?alt=json" + }, + "body": { + "kind": "storage#bucket", + "selfLink": "https://www.googleapis.com/storage/v1/b/c7n-pap-inh-r7t2wd5n", + "id": "c7n-pap-inh-r7t2wd5n", + "name": "c7n-pap-inh-r7t2wd5n", + "projectNumber": "000000000000", + "generation": "1791382958223744443", + "metageneration": "2", + "location": "US", + "storageClass": "STANDARD", + "etag": "CAI=", + "timeCreated": "2026-10-07T14:22:38.534Z", + "updated": "2026-10-07T14:22:40.498Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "softDeletePolicy": { + "retentionDurationSeconds": "604800", + "effectiveTime": "2026-10-07T14:22:38.534Z" + }, + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "publicAccessPrevention": "inherited" + }, + "locationType": "multi-region", + "rpo": "DEFAULT" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b_1.json new file mode 100644 index 00000000000..7f35866b64f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/get-storage.googleapis.com-storage-v1-b_1.json @@ -0,0 +1,54 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "date": "Wed, 07 Oct 2026 14:22:40 GMT", + "vary": "Origin, X-Origin", + "cache-control": "private, max-age=0, must-revalidate, no-transform", + "expires": "Wed, 07 Oct 2026 14:22:40 GMT", + "x-goog-gcs-base-ts": "1a116beb830", + "content-length": "5690", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200", + "content-location": "https://storage.googleapis.com/storage/v1/b?project=cloud-custodian&projection=full&alt=json" + }, + "body": { + "kind": "storage#buckets", + "items": [ + { + "kind": "storage#bucket", + "selfLink": "https://www.googleapis.com/storage/v1/b/c7n-pap-inh-r7t2wd5n", + "id": "c7n-pap-inh-r7t2wd5n", + "name": "c7n-pap-inh-r7t2wd5n", + "projectNumber": "000000000000", + "generation": "1791382958223744443", + "metageneration": "1", + "location": "US", + "storageClass": "STANDARD", + "etag": "CAE=", + "timeCreated": "2026-10-07T14:22:38.534Z", + "updated": "2026-10-07T14:22:38.534Z", + "labels": { + "goog-terraform-provisioned": "true" + }, + "softDeletePolicy": { + "retentionDurationSeconds": "604800", + "effectiveTime": "2026-10-07T14:22:38.534Z" + }, + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "publicAccessPrevention": "enforced" + }, + "locationType": "multi-region", + "rpo": "DEFAULT" + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/patch-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/patch-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json new file mode 100644 index 00000000000..a44520b4a5f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/bucket-public-access-inherited/patch-storage.googleapis.com-storage-v1-b-c7n-pap-inh-r7t2wd5n_1.json @@ -0,0 +1,29 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "etag": "CAI=", + "date": "Wed, 07 Oct 2026 14:22:40 GMT", + "vary": "Origin, X-Origin", + "cache-control": "no-cache, no-store, max-age=0, must-revalidate", + "expires": "Mon, 01 Jan 1990 00:00:00 GMT", + "pragma": "no-cache", + "x-goog-gcs-base-ts": "1a116beb997", + "content-length": "288", + "server": "UploadServer", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "status": "200" + }, + "body": { + "iamConfiguration": { + "bucketPolicyOnly": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "uniformBucketLevelAccess": { + "enabled": true, + "lockedTime": "2027-01-05T14:22:38.534Z" + }, + "publicAccessPrevention": "inherited" + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/main.tf b/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/main.tf new file mode 100644 index 00000000000..735541aea7a --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/main.tf @@ -0,0 +1,9 @@ +provider "google" {} + +resource "google_storage_bucket" "bucket" { + name = "c7n-pap-test-k3v9xq2m" + location = "US" + force_destroy = true + public_access_prevention = "inherited" + uniform_bucket_level_access = true +} diff --git a/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/tf_resources.json b/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/tf_resources.json new file mode 100644 index 00000000000..05e1a83cdbc --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/bucket_public_access_enforced/tf_resources.json @@ -0,0 +1,57 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_storage_bucket": { + "bucket": { + "autoclass": [], + "cors": [], + "custom_placement_config": [], + "default_event_based_hold": false, + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_object_retention": false, + "encryption": [], + "force_destroy": true, + "hierarchical_namespace": [ + { + "enabled": false + } + ], + "id": "c7n-pap-test-k3v9xq2m", + "ip_filter": [], + "labels": null, + "lifecycle_rule": [], + "location": "US", + "logging": [], + "name": "c7n-pap-test-k3v9xq2m", + "project": "cloud-custodian", + "project_number": 0, + "public_access_prevention": "inherited", + "requester_pays": false, + "retention_policy": [], + "rpo": "DEFAULT", + "self_link": "https://www.googleapis.com/storage/v1/b/c7n-pap-test-k3v9xq2m", + "soft_delete_policy": [ + { + "effective_time": "2026-10-07T14:06:14.192Z", + "retention_duration_seconds": 604800 + } + ], + "storage_class": "STANDARD", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "time_created": "2026-10-07T14:06:14.192Z", + "timeouts": null, + "uniform_bucket_level_access": true, + "updated": "2026-10-07T14:06:14.192Z", + "url": "gs://c7n-pap-test-k3v9xq2m", + "versioning": [], + "website": [] + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/main.tf b/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/main.tf new file mode 100644 index 00000000000..f4ffd219d1d --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/main.tf @@ -0,0 +1,9 @@ +provider "google" {} + +resource "google_storage_bucket" "bucket" { + name = "c7n-pap-inh-r7t2wd5n" + location = "US" + force_destroy = true + public_access_prevention = "enforced" + uniform_bucket_level_access = true +} diff --git a/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/tf_resources.json b/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/tf_resources.json new file mode 100644 index 00000000000..bbe7852a1d1 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/bucket_public_access_inherited/tf_resources.json @@ -0,0 +1,57 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_storage_bucket": { + "bucket": { + "autoclass": [], + "cors": [], + "custom_placement_config": [], + "default_event_based_hold": false, + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_object_retention": false, + "encryption": [], + "force_destroy": true, + "hierarchical_namespace": [ + { + "enabled": false + } + ], + "id": "c7n-pap-inh-r7t2wd5n", + "ip_filter": [], + "labels": null, + "lifecycle_rule": [], + "location": "US", + "logging": [], + "name": "c7n-pap-inh-r7t2wd5n", + "project": "cloud-custodian", + "project_number": 0, + "public_access_prevention": "enforced", + "requester_pays": false, + "retention_policy": [], + "rpo": "DEFAULT", + "self_link": "https://www.googleapis.com/storage/v1/b/c7n-pap-inh-r7t2wd5n", + "soft_delete_policy": [ + { + "effective_time": "2026-10-07T14:22:38.534Z", + "retention_duration_seconds": 604800 + } + ], + "storage_class": "STANDARD", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "time_created": "2026-10-07T14:22:38.534Z", + "timeouts": null, + "uniform_bucket_level_access": true, + "updated": "2026-10-07T14:22:38.534Z", + "url": "gs://c7n-pap-inh-r7t2wd5n", + "versioning": [], + "website": [] + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_gcp_storage.py b/tools/c7n_gcp/tests/test_gcp_storage.py index e7a478ec758..2098779dfc5 100644 --- a/tools/c7n_gcp/tests/test_gcp_storage.py +++ b/tools/c7n_gcp/tests/test_gcp_storage.py @@ -57,6 +57,55 @@ def test_bucket_set_iam_policy_remove_public_access(test, bucket_set_iam_policy) assert sa_email in all_members +@terraform("bucket_public_access_enforced") +def test_bucket_public_access_enforced(test, bucket_public_access_enforced): + bucket_name = ( + bucket_public_access_enforced.resources['google_storage_bucket']['bucket']['name']) + factory = test.replay_flight_data('bucket-public-access-enforced') + policy = test.load_policy( + {'name': 'bucket-public-access-enforced', + 'resource': 'gcp.bucket', + 'filters': [ + {'name': bucket_name}, + {'type': 'value', + 'key': 'iamConfiguration.publicAccessPrevention', + 'value': 'enforced', + 'op': 'ne'}], + 'actions': [{'type': 'set-public-access-prevention', 'state': 'enforced'}]}, + session_factory=factory) + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['name'] == bucket_name + + bucket = policy.resource_manager.get_resource({'bucket_name': bucket_name}) + assert bucket['iamConfiguration']['publicAccessPrevention'] == 'enforced' + # patching public access prevention must not reset uniform access + assert bucket['iamConfiguration']['uniformBucketLevelAccess']['enabled'] is True + + +@terraform("bucket_public_access_inherited") +def test_bucket_public_access_inherited(test, bucket_public_access_inherited): + bucket_name = ( + bucket_public_access_inherited.resources['google_storage_bucket']['bucket']['name']) + factory = test.replay_flight_data('bucket-public-access-inherited') + policy = test.load_policy( + {'name': 'bucket-public-access-inherited', + 'resource': 'gcp.bucket', + 'filters': [ + {'name': bucket_name}, + {'iamConfiguration.publicAccessPrevention': 'enforced'}], + 'actions': [{'type': 'set-public-access-prevention', 'state': 'inherited'}]}, + session_factory=factory) + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['name'] == bucket_name + + bucket = policy.resource_manager.get_resource({'bucket_name': bucket_name}) + assert bucket['iamConfiguration']['publicAccessPrevention'] == 'inherited' + # patching public access prevention must not reset uniform access + assert bucket['iamConfiguration']['uniformBucketLevelAccess']['enabled'] is True + + @terraform("bucket_set_iam_policy") def test_bucket_set_iam_policy_add_bindings(test, bucket_set_iam_policy): bucket_name = bucket_set_iam_policy.resources['google_storage_bucket']['bucket']['name'] From a75de191ac0601aabad825338b053ad6f9f8de58 Mon Sep 17 00:00:00 2001 From: dreakj12 Date: Fri, 9 Oct 2026 09:39:27 -0400 Subject: [PATCH 34/65] gcp - vertex-ai-publisher-model - add metrics filter support (#11107) --- tools/c7n_gcp/c7n_gcp/filters/metrics.py | 23 +- tools/c7n_gcp/c7n_gcp/query.py | 4 + tools/c7n_gcp/c7n_gcp/resources/vertexai.py | 49 +- ...v1beta1-publishers-anthropic-models_1.json | 567 ++++++++++++++++++ ...om-v1beta1-publishers-cohere-models_1.json | 18 + ...om-v1beta1-publishers-google-models_1.json | 298 +++++++++ ....com-v1beta1-publishers-meta-models_1.json | 18 + ...v1beta1-publishers-mistralai-models_1.json | 121 ++++ ...projects-cloud-custodian-timeSeries_1.json | 47 ++ tools/c7n_gcp/tests/test_vertexai.py | 189 +++++- 10 files changed, 1311 insertions(+), 23 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-anthropic-models_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-cohere-models_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-google-models_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-meta-models_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-mistralai-models_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json diff --git a/tools/c7n_gcp/c7n_gcp/filters/metrics.py b/tools/c7n_gcp/c7n_gcp/filters/metrics.py index eacf9bc1802..64cf0ad8332 100644 --- a/tools/c7n_gcp/c7n_gcp/filters/metrics.py +++ b/tools/c7n_gcp/c7n_gcp/filters/metrics.py @@ -113,10 +113,21 @@ class GCPMetricsFilter(Filter): permissions = ("monitoring.timeSeries.list",) def validate(self): - if not self.data.get('metric-key') and \ - not hasattr(self.manager.resource_type, 'metric_key'): - raise FilterValidationError("metric-key not defined for resource %s," - "so must be provided in the policy" % (self.manager.type)) + metric_key = self.data.get('metric-key') + if not metric_key and not hasattr(self.manager.resource_type, 'metric_key'): + raise FilterValidationError( + "metric-key not defined for resource %s, " + "so must be provided in the policy" % (self.manager.type)) + + supported_metric_keys = self.manager.resource_type.supported_metric_keys + if ( + metric_key and + supported_metric_keys is not None and + metric_key not in supported_metric_keys + ): + raise FilterValidationError( + "metric-key '%s' not supported for resource %s, supported keys: %s" % ( + metric_key, self.manager.type, ', '.join(supported_metric_keys))) return self @@ -237,8 +248,8 @@ def process_resource(self, resource): @classmethod def register_resources(klass, registry, resource_class): if resource_class.filter_registry: - if resource_class.resource_type.allow_metrics_filters and \ - 'metrics' not in resource_class.filter_registry: + if (resource_class.resource_type.allow_metrics_filters + and 'metrics' not in resource_class.filter_registry): resource_class.filter_registry.register('metrics', klass) diff --git a/tools/c7n_gcp/c7n_gcp/query.py b/tools/c7n_gcp/c7n_gcp/query.py index e4a96626abd..c180a9d601f 100644 --- a/tools/c7n_gcp/c7n_gcp/query.py +++ b/tools/c7n_gcp/c7n_gcp/query.py @@ -453,6 +453,10 @@ class TypeInfo(metaclass=TypeMeta): # Assume they generally can, and flip this to `False` for those that don't. allow_metrics_filters = True + # Metric keys a resource type accepts in an explicit `metric-key` policy + # option, including its default `metric_key`. `None` means no restriction. + supported_metric_keys = None + @classmethod def get_metric_resource_name(cls, resource, metric_key=None): return resource.get(cls.name) diff --git a/tools/c7n_gcp/c7n_gcp/resources/vertexai.py b/tools/c7n_gcp/c7n_gcp/resources/vertexai.py index a80936764f5..6e7368e78a4 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/vertexai.py +++ b/tools/c7n_gcp/c7n_gcp/resources/vertexai.py @@ -9,10 +9,8 @@ from googleapiclient.errors import HttpError import yaml -from c7n.filters.core import FilterValidationError from c7n.utils import local_session, jmespath_search, type_schema from c7n_gcp.actions import MethodAction -from c7n_gcp.filters.metrics import GCPMetricsFilter from c7n_gcp.provider import resources from c7n_gcp.query import ( config_regions, QueryResourceManager, TypeInfo, ChildResourceManager, ChildTypeInfo) @@ -299,6 +297,7 @@ class resource_type(VertexAITypeInfo): permissions = ('aiplatform.endpoints.list',) urn_component = 'endpoint' metric_key = 'resource.labels.endpoint_id' + supported_metric_keys = ('resource.labels.endpoint_id',) @classmethod def get_metric_resource_name(cls, resource, metric_key=None): @@ -306,19 +305,6 @@ def get_metric_resource_name(cls, resource, metric_key=None): return resource['name'].split('/')[-1] -@VertexAIEndpoint.filter_registry.register('metrics') -class VertexAIEndpointMetricsFilter(GCPMetricsFilter): - - def validate(self): - super().validate() - metric_key = self.data.get('metric-key') - if metric_key and metric_key != self.manager.resource_type.metric_key: - raise FilterValidationError( - "vertex-ai-endpoint metrics filter only supports " - f"metric-key '{self.manager.resource_type.metric_key}', got '{metric_key}'") - return self - - @VertexAIEndpoint.action_registry.register('monitor') class VertexAIEndpointMonitor(MethodAction): """Create Model Deployment Monitoring Jobs for Vertex AI Endpoints @@ -1445,6 +1431,30 @@ class VertexAIPublisherModel(ChildResourceManager): key: name op: regex value: 'publishers/anthropic/.*' + + :example: Find models that used more than 1,000,000 input tokens in the last day + + Cloud Monitoring returns one ``token_count`` time series per combination + of metric labels, such as ``type`` (``input`` or ``output``) and + ``source`` region. Filter on the labels you want, then reduce the + remaining series to one value per model. + + .. code-block:: yaml + + policies: + - name: vertex-ai-publisher-model-input-tokens + resource: gcp.vertex-ai-publisher-model + filters: + - type: metrics + name: aiplatform.googleapis.com/publisher/online_serving/token_count + filter: metric.labels.type = "input" + aligner: ALIGN_SUM + reducer: REDUCE_SUM + group-by-fields: + - resource.labels.model_user_id + days: 1 + op: greater-than + value: 1000000 """ def get_permissions(self): @@ -1472,3 +1482,12 @@ class resource_type(ChildTypeInfo): urn_component = 'publisher-model' urn_id_segments = (-1,) parent_spec = {'resource': 'vertex-ai-publisher'} + metric_key = 'resource.labels.model_user_id' + supported_metric_keys = ('resource.labels.model_user_id',) + + @classmethod + def get_metric_resource_name(cls, resource, metric_key=None): + # Monitoring labels the metric with the bare model id, the + # last part of the resource's full + # publishers/{publisher}/models/{model} name. + return resource['name'].split('/')[-1] diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-anthropic-models_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-anthropic-models_1.json new file mode 100644 index 00000000000..7e6da36da98 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-anthropic-models_1.json @@ -0,0 +1,567 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:04 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "21367", + "-content-encoding": "gzip", + "content-location": "https://aiplatform.googleapis.com/v1beta1/publishers/anthropic/models?alt=json" + }, + "body": { + "publisherModels": [ + { + "name": "publishers/anthropic/models/claude-haiku-4-5", + "versionId": "20251001", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-haiku-4-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-haiku-4-5&mp=anthropic/anthropic-867.cloudpartnerservices.goog&service=anthropic-867.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-4-5", + "versionId": "20251101", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-4-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-4-5&mp=anthropic/anthropic-874.cloudpartnerservices.goog&service=anthropic-874.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-4-6", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-4-6" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-4-6&mp=anthropic/anthropic-882.cloudpartnerservices.goog&service=anthropic-882.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-sonnet-4-6", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-sonnet-4-6" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-sonnet-4-6&mp=anthropic/anthropic-884.cloudpartnerservices.goog&service=anthropic-884.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-4-7", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-4-7" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-4-7&mp=anthropic/anthropic-895.cloudpartnerservices.goog&service=anthropic-895.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-sonnet-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-sonnet-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-sonnet-5&mp=anthropic/anthropic-896.cloudpartnerservices.goog&service=anthropic-896.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-4-8", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-4-8" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-4-8&mp=anthropic/anthropic-897.cloudpartnerservices.goog&service=anthropic-897.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-fable-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-fable-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-fable-5&mp=anthropic/anthropic-902.cloudpartnerservices.goog&service=anthropic-902.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&advanceAISafetyAddendum=true&zdrExemption=902" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-5&mp=anthropic/anthropic-904.cloudpartnerservices.goog&service=anthropic-904.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true&dataSharing=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-fable-5-1", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-fable-5-1" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-fable-5-1&mp=anthropic/anthropic-912.cloudpartnerservices.goog&service=anthropic-912.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&advanceAISafetyAddendum=true&zdrExemption=912" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-opus-5-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-opus-5-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-opus-5-5&mp=anthropic/anthropic-914.cloudpartnerservices.goog&service=anthropic-914.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true&dataSharing=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-sonnet-5-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-sonnet-5-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-sonnet-5-5&mp=anthropic/anthropic-915.cloudpartnerservices.goog&service=anthropic-915.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true&dataSharing=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/anthropic/models/claude-haiku-5-5", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=claude-haiku-5-5" + } + } + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/anthropic/models/claude-haiku-5-5&mp=anthropic/anthropic-932.cloudpartnerservices.goog&service=anthropic-932.cloudpartnerservices.goog&enableLoggingDisclaimer=true&enableWebsearchDisclaimer=true&optionalAdvanceAISafetyAddendum=true&dataSharing=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/anthropic_claude_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-cohere-models_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-cohere-models_1.json new file mode 100644 index 00000000000..9e977f4d234 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-cohere-models_1.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:05 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://aiplatform.googleapis.com/v1beta1/publishers/cohere/models?alt=json" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-google-models_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-google-models_1.json new file mode 100644 index 00000000000..93432ace491 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-google-models_1.json @@ -0,0 +1,298 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:04 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "12050", + "-content-encoding": "gzip", + "content-location": "https://aiplatform.googleapis.com/v1beta1/publishers/google/models?alt=json" + }, + "body": { + "publisherModels": [ + { + "name": "publishers/google/models/gemini-1.5-pro-002", + "versionId": "default", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/retail/product_attributes_extraction.ipynb" + } + }, + "title": "Open Notebook", + "resourceTitle": "Notebook", + "resourceUseCase": "Product Attributes Extraction", + "resourceDescription": "Extract product descriptions and attribute json from images using Gemini 1.5 Pro. This notebook also shows the use of self-correcting prompt to improve the quality of the output." + }, + "openGenerationAiStudio": { + "references": { + "us-central1": { + "uri": "https://console.cloud.google.com/vertex-ai/studio/freeform?model=gemini-1.5-pro-002" + } + } + }, + "openEvaluationPipeline": { + "references": { + "us-central1": { + "uri": "https://console.cloud.google.com/vertex-ai/pipelines/vertex-ai-templates/autosxs-template" + } + }, + "title": "Evaluate" + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_1_5_pro.ipynb" + } + }, + "title": "Open Notebook" + }, + { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_1_5_pro.ipynb" + } + }, + "title": "Open Notebook", + "resourceTitle": "Notebook", + "resourceUseCase": "Vertex AI Gemini API 1.5 Pro", + "resourceDescription": "Use the Vertex AI Gemini API 1.5 Pro model to process images, video, audio, and text simultaneously." + }, + { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/retail/product_attributes_extraction.ipynb" + } + }, + "title": "Open Notebook", + "resourceTitle": "Notebook", + "resourceUseCase": "Product Attributes Extraction", + "resourceDescription": "Extract product descriptions and attribute json from images using Gemini 1.5 Pro. This notebook also shows the use of self-correcting prompt to improve the quality of the output." + } + ] + } + }, + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-1.5-pro-002@default" + }, + { + "name": "publishers/google/models/gemini-2.5-flash-preview-04-17", + "versionId": "default", + "supportedActions": { + "openGenerationAiStudio": { + "references": { + "us-central1": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=gemini-2.5-flash-preview-04-17" + } + } + } + }, + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash-preview-04-17@default" + }, + { + "name": "publishers/google/models/gemini-2.5-pro-exp-03-25", + "versionId": "default", + "supportedActions": { + "openNotebook": { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_2_5_pro.ipynb" + } + }, + "resourceTitle": "Notebook", + "resourceUseCase": "Vertex Serving", + "resourceDescription": "Intro to Gemini 2.5 Pro." + }, + "openGenerationAiStudio": { + "references": { + "us-central1": { + "uri": "https://console.cloud.google.com/vertex-ai/generative/multimodal/create/text?model=gemini-2.5-pro-exp-03-25" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "us-central1": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/generative-ai/blob/main/gemini/getting-started/intro_gemini_2_5_pro.ipynb" + } + }, + "resourceTitle": "Notebook", + "resourceUseCase": "Vertex Serving", + "resourceDescription": "Intro to Gemini 2.5 Pro." + } + ] + } + }, + "launchStage": "EXPERIMENTAL", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-pro-exp-03-25@default" + }, + { + "name": "publishers/google/models/gemini-2.5-pro", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-pro@default" + }, + { + "name": "publishers/google/models/gemini-2.5-flash", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash@default" + }, + { + "name": "publishers/google/models/gemini-2.5-flash-lite", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash-lite@default" + }, + { + "name": "publishers/google/models/gemini-2.5-pro-tts", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-pro-tts@default" + }, + { + "name": "publishers/google/models/gemini-2.5-flash-tts", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-2.5-flash-tts@default" + }, + { + "name": "publishers/google/models/gemini-live-2.5-flash-native-audio", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-live-2.5-flash-native-audio@default" + }, + { + "name": "publishers/google/models/gemini-3-flash-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3-flash-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.1-flash-image-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.1-flash-image-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.1-pro-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.1-pro-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.5-flash", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.5-flash@default" + }, + { + "name": "publishers/google/models/gemini-3.1-flash-image", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.1-flash-image@default" + }, + { + "name": "publishers/google/models/gemini-3-pro-image", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3-pro-image@default" + }, + { + "name": "publishers/google/models/gemini-embedding-2", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-embedding-2@default" + }, + { + "name": "publishers/google/models/gemini-3.1-flash-lite", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.1-flash-lite@default" + }, + { + "name": "publishers/google/models/gemini-3.1-flash-lite-image", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.1-flash-lite-image@default" + }, + { + "name": "publishers/google/models/gemini-3.5-transcribe-live-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.5-transcribe-live-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.5-transcribe-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.5-transcribe-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.5-flash-lite", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.5-flash-lite@default" + }, + { + "name": "publishers/google/models/gemini-3.6-flash", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.6-flash@default" + }, + { + "name": "publishers/google/models/gemini-3.5-live-translate-preview", + "versionId": "default", + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.5-live-translate-preview@default" + }, + { + "name": "publishers/google/models/gemini-omni-1.1-flash-preview", + "versionId": "default", + "supportedActions": { + "openGenerationAiStudio": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/agent-platform/studio/multimodal?model=gemini-omni-1.1-flash-preview" + } + }, + "title": "Open Agent Studio" + } + }, + "launchStage": "PUBLIC_PREVIEW", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-omni-1.1-flash-preview@default" + }, + { + "name": "publishers/google/models/gemini-3.7-flash", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.7-flash@default" + }, + { + "name": "publishers/google/models/gemini-3.8-flash", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-3.8-flash@default" + }, + { + "name": "publishers/google/models/gemini-nano-banana-2.1", + "versionId": "default", + "launchStage": "GA", + "publisherModelTemplate": "projects/{project}/locations/{location}/publishers/google/models/gemini-nano-banana-2.1@default" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-meta-models_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-meta-models_1.json new file mode 100644 index 00000000000..218dd3f8ac4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-meta-models_1.json @@ -0,0 +1,18 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:04 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3", + "-content-encoding": "gzip", + "content-location": "https://aiplatform.googleapis.com/v1beta1/publishers/meta/models?alt=json" + }, + "body": {} +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-mistralai-models_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-mistralai-models_1.json new file mode 100644 index 00000000000..ee5d5ad904d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-aiplatform.googleapis.com-v1beta1-publishers-mistralai-models_1.json @@ -0,0 +1,121 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:05 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3856", + "-content-encoding": "gzip", + "content-location": "https://aiplatform.googleapis.com/v1beta1/publishers/mistralai/models?alt=json" + }, + "body": { + "publisherModels": [ + { + "name": "publishers/mistralai/models/mistral-small-2503", + "versionId": "001", + "openSourceCategory": "THIRD_PARTY_OWNED_OSS", + "supportedActions": { + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/mistralai/models/mistral-small-2503&mp=mistralai/mistral-small-2503.cloudpartnerservices.goog&service=mistral-small-2503.cloudpartnerservices.goog&skip_questionnaire=true" + } + } + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/mistralai/models/mistral-ocr-2505", + "versionId": "001", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/mistralai/models/mistral-ocr-2505&mp=mistralai/mistral-ocr-2505.cloudpartnerservices.goog&service=mistral-ocr-2505.cloudpartnerservices.goog&skip_questionnaire=true" + } + } + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/mistralai/models/mistral-medium-3", + "versionId": "001", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/mistralai_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/mistralai/models/mistral-medium-3&mp=mistralai/mistral-medium-3.cloudpartnerservices.goog&service=mistral-medium-3.cloudpartnerservices.goog&skip_questionnaire=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/mistralai_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + }, + { + "name": "publishers/mistralai/models/codestral-2", + "versionId": "001", + "openSourceCategory": "PROPRIETARY", + "supportedActions": { + "openNotebook": { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/mistralai_intro.ipynb" + } + }, + "title": "Open Notebook" + }, + "requestAccess": { + "references": { + "global": { + "uri": "https://console.cloud.google.com/vertex-ai/model-garden/questionnaire?model=publishers/mistralai/models/codestral-2&mp=mistralai/mistralai-869.cloudpartnerservices.goog&service=mistralai-869.cloudpartnerservices.goog&skip_questionnaire=true" + } + } + }, + "openNotebooks": { + "notebooks": [ + { + "references": { + "global": { + "uri": "https://colab.research.google.com/github/GoogleCloudPlatform/vertex-ai-samples/blob/main/notebooks/official/generative_ai/mistralai_intro.ipynb" + } + }, + "title": "Open Notebook" + } + ] + } + }, + "launchStage": "GA" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json new file mode 100644 index 00000000000..4fbf4135b53 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/vertex-ai-publisher-model-metrics/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json @@ -0,0 +1,47 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 01:01:05 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "670", + "-content-encoding": "gzip", + "content-location": "https://monitoring.googleapis.com/v3/projects/cloud-custodian/timeSeries?filter=metric.type+%3D+%22aiplatform.googleapis.com%2Fpublisher%2Fonline_serving%2Ftoken_count%22+AND+%28+resource.labels.model_user_id+%3D+%22gemini-2.5-flash%22+%29++AND+metric.labels.type+%3D+%22input%22&interval.startTime=2026-10-08T01%3A01%3A05Z&interval.endTime=2026-10-09T01%3A01%3A05Z&aggregation.alignmentPeriod=86400.0s&aggregation.perSeriesAligner=ALIGN_SUM&aggregation.crossSeriesReducer=REDUCE_SUM&aggregation.groupByFields=resource.labels.model_user_id&view=FULL&alt=json" + }, + "body": { + "timeSeries": [ + { + "metric": { + "type": "aiplatform.googleapis.com/publisher/online_serving/token_count" + }, + "resource": { + "type": "aiplatform.googleapis.com/PublisherModel", + "labels": { + "model_user_id": "gemini-2.5-flash", + "project_id": "cloud-custodian" + } + }, + "metricKind": "DELTA", + "valueType": "INT64", + "points": [ + { + "interval": { + "startTime": "2026-10-08T01:01:05Z", + "endTime": "2026-10-09T01:01:05Z" + }, + "value": { + "int64Value": "15" + } + } + ] + } + ], + "unit": "1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_vertexai.py b/tools/c7n_gcp/tests/test_vertexai.py index eb4c60d8283..6c394d97bcb 100644 --- a/tools/c7n_gcp/tests/test_vertexai.py +++ b/tools/c7n_gcp/tests/test_vertexai.py @@ -5,6 +5,7 @@ import json import time import logging +from datetime import datetime, timedelta, timezone from unittest.mock import Mock, patch import pytest from google.api_core.client_options import ClientOptions @@ -13,7 +14,7 @@ from c7n.filters.core import FilterValidationError from c7n_gcp.client import get_default_project from gcp_common import BaseTest -from c7n_gcp.resources.vertexai import VertexAIEndpoint +from c7n_gcp.resources.vertexai import VertexAIEndpoint, VertexAIPublisherModel def _make_http_error( @@ -730,10 +731,128 @@ def test_vertexai_endpoint_metric_resource_name(): ) +def _generate_publisher_model_traffic(project_id, location, model, calls=5): + """Send generateContent calls to a Model Garden model. + + Only runs during recording, since Cloud Monitoring's token_count + metric has no data until the model gets requests. + """ + import google.auth + from google.auth.transport.requests import AuthorizedSession + + credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/cloud-platform']) + session = AuthorizedSession(credentials) + url = ( + f'https://{location}-aiplatform.googleapis.com/v1/projects/' + f'{project_id}/locations/{location}/publishers/google/models/' + f'{model}:generateContent' + ) + body = {'contents': [{'role': 'user', 'parts': [{'text': 'Say OK.'}]}]} + for _ in range(calls): + resp = session.post(url, json=body, timeout=30) + resp.raise_for_status() + + +def _wait_for_publisher_model_metric( + project_id, metric_type, model, timeout=600, interval=30): + """Poll Cloud Monitoring until ``metric_type`` has a data point for + ``model``. Avoids guessing a fixed propagation delay. + + Keeps polling through throttling, server errors, and network errors, + so one transient failure doesn't discard the recording. + """ + import google.auth + import requests + from google.auth.transport.requests import AuthorizedSession + + credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/cloud-platform']) + session = AuthorizedSession(credentials) + url = f'https://monitoring.googleapis.com/v3/projects/{project_id}/timeSeries' + now = datetime.now(timezone.utc).replace(microsecond=0) + params = { + 'filter': ( + f'metric.type = "{metric_type}" AND ' + f'resource.labels.model_user_id = "{model}"' + ), + 'interval.startTime': (now - timedelta(hours=6)).isoformat(), + 'interval.endTime': now.isoformat(), + 'view': 'FULL', + } + deadline = time.time() + timeout + while True: + try: + resp = session.get(url, params=params, timeout=30) + except (requests.ConnectionError, requests.Timeout): + resp = None + if resp is not None and resp.status_code != 429 and resp.status_code < 500: + resp.raise_for_status() + if resp.json().get('timeSeries'): + return + if time.time() >= deadline: + raise TimeoutError( + f'{metric_type} did not appear for {model} ' + f'within {timeout}s of polling') + time.sleep(interval) + + +def test_vertexai_publisher_model_metrics(test): + """Test the metrics filter against a live publisher-model metric. + + Recording this test sends Gemini calls, then polls Cloud Monitoring + until ``token_count`` shows up for the model, instead of a fixed + sleep. See ``_generate_publisher_model_traffic`` and + ``_wait_for_publisher_model_metric`` above. + """ + project_id = get_default_project() + location = 'us-central1' + model = 'gemini-2.5-flash' + metric_type = 'aiplatform.googleapis.com/publisher/online_serving/token_count' + + session_factory = test.replay_flight_data( + 'vertex-ai-publisher-model-metrics') + + if test.recording: + _generate_publisher_model_traffic(project_id, location, model) + _wait_for_publisher_model_metric(project_id, metric_type, model) + + policy = test.load_policy( + { + 'name': 'publisher-model-token-count', + 'resource': 'gcp.vertex-ai-publisher-model', + 'filters': [ + {'type': 'value', 'key': 'name', + 'value': f'publishers/google/models/{model}'}, + { + 'type': 'metrics', + 'name': metric_type, + 'filter': 'metric.labels.type = "input"', + 'aligner': 'ALIGN_SUM', + 'reducer': 'REDUCE_SUM', + 'group-by-fields': ['resource.labels.model_user_id'], + 'days': 1, + 'op': 'greater-than', + 'value': 0, + }, + ], + }, + session_factory=session_factory, + ) + + resources = policy.run() + + assert len(resources) == 1 + metric_name = f'{metric_type}.ALIGN_SUM.REDUCE_SUM' + metric = resources[0]['c7n.metrics'][metric_name] + assert metric['resource']['labels']['model_user_id'] == model + assert len(metric['points']) == 1 + + def test_vertexai_endpoint_metrics_invalid_metric_key(test): with pytest.raises( FilterValidationError, - match="only supports metric-key 'resource.labels.endpoint_id'", + match="metric-key 'metric.labels.deployed_model_id' not supported", ): test.load_policy({ 'name': 'vertexai-endpoint-invalid-metric-key', @@ -751,6 +870,26 @@ def test_vertexai_endpoint_metrics_invalid_metric_key(test): }, validate=True) +def test_vertexai_publisher_model_metrics_invalid_metric_key(test): + with pytest.raises( + FilterValidationError, + match="metric-key 'metric.labels.type' not supported", + ): + test.load_policy({ + 'name': 'vertexai-publisher-model-invalid-metric-key', + 'resource': 'gcp.vertex-ai-publisher-model', + 'filters': [ + { + 'type': 'metrics', + 'name': 'aiplatform.googleapis.com/publisher/online_serving/token_count', + 'metric-key': 'metric.labels.type', + 'op': 'greater-than', + 'value': 0, + }, + ], + }, validate=True) + + @terraform("vertexai_endpoint_metrics") def test_vertexai_endpoint_metrics(test, vertexai_endpoint_metrics): """ @@ -1888,6 +2027,52 @@ def test_publisher_model_non_google_publisher(self): f'Model {resource.get("name")} unexpectedly matched Gemini pattern' ) + def test_publisher_model_metric_resource_name(self): + """Test get_metric_resource_name derives the bare model id + across a few publishers. + """ + cases = [ + ('publishers/google/models/gemini-1.5-pro', 'gemini-1.5-pro'), + ('publishers/anthropic/models/claude-sonnet-4-5', 'claude-sonnet-4-5'), + ('publishers/meta/models/llama-3.1-405b-instruct-maas', + 'llama-3.1-405b-instruct-maas'), + ] + for name, expected in cases: + resource = {'name': name} + self.assertEqual( + VertexAIPublisherModel.resource_type.get_metric_resource_name(resource), + expected) + # Explicitly passing the metric key works too. + self.assertEqual( + VertexAIPublisherModel.resource_type.get_metric_resource_name( + resource, metric_key='resource.labels.model_user_id'), + expected) + + def test_publisher_model_metrics_filter_registration(self): + """Test the metrics filter registers on publisher models and + defaults metric-key to the model id label.""" + policy = self.load_policy( + {'name': 'publisher-model-dedicated-capacity', + 'resource': 'gcp.vertex-ai-publisher-model', + 'filters': [ + {'type': 'metrics', + 'name': 'aiplatform.googleapis.com/publisher/online_serving/' + 'dedicated_token_limit', + 'aligner': 'ALIGN_MAX', + 'reducer': 'REDUCE_MAX', + 'group-by-fields': [ + 'resource.labels.resource_container', + 'resource.labels.publisher', + 'resource.labels.model_user_id', + ], + 'days': 1, + 'op': 'gt', + 'value': 0} + ]}) + self.assertEqual( + policy.resource_manager.resource_type.metric_key, + 'resource.labels.model_user_id') + @terraform('vertexai_endpoint_get_resource') def test_vertexai_endpoint_get_resource(test, vertexai_endpoint_get_resource): From 4ec03b6f3b3c0dcf7af13892912ca15c44aa8528 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 13 Oct 2026 10:59:19 -0600 Subject: [PATCH 35/65] aws - ec2 - update tag back-fill and enable for cloud trail mode (#11064) --------- Co-authored-by: Claude Opus 5 (1M context) --- c7n/resources/ec2.py | 42 +- .../ec2.DescribeInstances_1.json | 199 +++++++++ .../ec2.DescribeInstances_1.json | 199 +++++++++ .../ec2.DescribeTags_1.json | 26 ++ .../ec2.DescribeInstances_1.json | 390 +++++++++++++++++ .../ec2.DescribeTags_1.json | 39 ++ .../ec2.DescribeTags_2.json | 14 + .../ec2.DescribeInstances_1.json | 199 +++++++++ tests/terraform/ec2_augment_tags/ami.tf | 12 + tests/terraform/ec2_augment_tags/main.tf | 23 + tests/terraform/ec2_augment_tags/network.tf | 8 + .../ec2_augment_tags/tf_resources.json | 408 ++++++++++++++++++ tests/test_ec2.py | 141 ++++++ 13 files changed, 1690 insertions(+), 10 deletions(-) create mode 100644 tests/data/placebo/test_ec2_augment_tags_instance_state/ec2.DescribeInstances_1.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeInstances_1.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeTags_1.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeInstances_1.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_1.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_2.json create mode 100644 tests/data/placebo/test_ec2_augment_tags_short_circuit/ec2.DescribeInstances_1.json create mode 100644 tests/terraform/ec2_augment_tags/ami.tf create mode 100644 tests/terraform/ec2_augment_tags/main.tf create mode 100644 tests/terraform/ec2_augment_tags/network.tf create mode 100644 tests/terraform/ec2_augment_tags/tf_resources.json diff --git a/c7n/resources/ec2.py b/c7n/resources/ec2.py index 3fe256fa63d..f373558e846 100644 --- a/c7n/resources/ec2.py +++ b/c7n/resources/ec2.py @@ -43,6 +43,13 @@ actions = ActionRegistry('ec2.actions') +# Maximum number of instances to look tags up by id for. Above this, fall back +# to the region-wide resource-type filter: ec2 allows only 200 total filter +# values per request, and this keeps the response bounded (100 ids, at ec2's +# limit of 50 tags each). +EC2_TAG_AUGMENT_BY_INSTANCES_MAX = 100 + + class DescribeEC2(query.DescribeSource): def get_query_params(self, query_params): @@ -70,7 +77,7 @@ def get_resources(self, ids, cache=True): reservations = self.query.get(self.manager, ids) return self._flatten_reservations(reservations) - def augment(self, resources): + def augment(self, resources, *, ec2_tag_augment_by_instances_max=None): """EC2 API and AWOL Tags While ec2 api generally returns tags when doing describe_x on for @@ -85,8 +92,7 @@ def augment(self, resources): always get tags from describe_x calls. """ if not resources or self.manager.data.get( - 'mode', {}).get('type', '') in ( - 'cloudtrail', 'ec2-instance-state'): + 'mode', {}).get('type', '') == 'ec2-instance-state': return resources # AWOL detector, so we don't make extraneous api calls. @@ -103,21 +109,37 @@ def augment(self, resources): if found: return resources - # Okay go and do the tag lookup + # Tag lookup client = utils.local_session(self.manager.session_factory).client('ec2') - tag_set = self.manager.retry( - client.describe_tags, - Filters=[{'Name': 'resource-type', - 'Values': ['instance']}])['Tags'] + model = self.manager.get_model() + ec2_tag_augment_by_instances_max = ( + ec2_tag_augment_by_instances_max or EC2_TAG_AUGMENT_BY_INSTANCES_MAX) + if len(resources) <= ec2_tag_augment_by_instances_max: + # ec2 ignores MaxResults for a resource-id filter, so no paginator. + tag_set = self.manager.retry( + client.describe_tags, + Filters=[{'Name': 'resource-id', + 'Values': [r[model.id] for r in resources]}])['Tags'] + else: + # Potentially many instances and tags, so paginate. + # Note that we don't specify a page size, and we never saw + # a continuation token without one -- but this covers every + # instance tag in the region, so the volume is unbounded and + # the paginator is insurance. + paginator = client.get_paginator('describe_tags') + paginator.PAGE_ITERATOR_CLS = query.RetryPageIterator + tag_set = paginator.paginate( + Filters=[{'Name': 'resource-type', + 'Values': ['instance']}]).build_full_result()['Tags'] + resource_tags = {} for t in tag_set: t.pop('ResourceType') rid = t.pop('ResourceId') resource_tags.setdefault(rid, []).append(t) - m = self.manager.get_model() for r in resources: - r['Tags'] = resource_tags.get(r[m.id], []) + r['Tags'] = resource_tags.get(r[model.id], []) return resources diff --git a/tests/data/placebo/test_ec2_augment_tags_instance_state/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_augment_tags_instance_state/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..c8cf6237c39 --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_instance_state/ec2.DescribeInstances_1.json @@ -0,0 +1,199 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-0bf0c1883f8890732", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-07758c92d403d0fd0", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-mOXgODjveTJZr9d5ucYvZJTAVj", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-098cf2cc45e8d5b7c", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:ff:e8:8e:01:a7", + "NetworkInterfaceId": "eni-0b119f9162c9c3ebf", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.79", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.79" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "Owner", + "Value": "robot" + }, + { + "Key": "Name", + "Value": "custodian-tester" + }, + { + "Key": "Env", + "Value": "Production" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0f0b98cda19cde0a0", + "ImageId": "ami-0f8848d57eb4fe2d2", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-79.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1b" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "PrivateIpAddress": "10.0.1.79" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..c8cf6237c39 --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeInstances_1.json @@ -0,0 +1,199 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-0bf0c1883f8890732", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-07758c92d403d0fd0", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-mOXgODjveTJZr9d5ucYvZJTAVj", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-098cf2cc45e8d5b7c", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:ff:e8:8e:01:a7", + "NetworkInterfaceId": "eni-0b119f9162c9c3ebf", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.79", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.79" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "Owner", + "Value": "robot" + }, + { + "Key": "Name", + "Value": "custodian-tester" + }, + { + "Key": "Env", + "Value": "Production" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0f0b98cda19cde0a0", + "ImageId": "ami-0f8848d57eb4fe2d2", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-79.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1b" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "PrivateIpAddress": "10.0.1.79" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeTags_1.json b/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeTags_1.json new file mode 100644 index 00000000000..d2e16577cca --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_lookup/ec2.DescribeTags_1.json @@ -0,0 +1,26 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "Env", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "Production" + }, + { + "Key": "Name", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "custodian-tester" + }, + { + "Key": "Owner", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "robot" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..c2683e710f8 --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeInstances_1.json @@ -0,0 +1,390 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-0bf0c1883f8890732", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-07758c92d403d0fd0", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-mOXgODjveTJZr9d5ucYvZJTAVj", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-098cf2cc45e8d5b7c", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:ff:e8:8e:01:a7", + "NetworkInterfaceId": "eni-0b119f9162c9c3ebf", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.79", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.79" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "Owner", + "Value": "robot" + }, + { + "Key": "Name", + "Value": "custodian-tester" + }, + { + "Key": "Env", + "Value": "Production" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0f0b98cda19cde0a0", + "ImageId": "ami-0f8848d57eb4fe2d2", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-79.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1b" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "PrivateIpAddress": "10.0.1.79" + } + ] + }, + { + "ReservationId": "r-0a992cfce2d63b907", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-01a4a812d6e1e264f", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-1DfjHdjLW8yBj5TNoqSuue7QbH", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-0cbcb92f03d9b8a8d", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:ff:df:ea:53:e3", + "NetworkInterfaceId": "eni-0707f5ab16555de2a", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.122", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.122" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "Name", + "Value": "custodian-tester" + }, + { + "Key": "Env", + "Value": "Production" + }, + { + "Key": "Owner", + "Value": "robot" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0877d42a6928968d2", + "ImageId": "ami-0f8848d57eb4fe2d2", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-122.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1b" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "PrivateIpAddress": "10.0.1.122" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_1.json b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_1.json new file mode 100644 index 00000000000..ebd5290ed3b --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_1.json @@ -0,0 +1,39 @@ +{ + "status_code": 200, + "data": { + "NextToken": "JSRBVWFSv9V9tDh2X4LtUh9yYA1BcVcjtHhw1A87nXE4DLZ5UeqHkrmce66avxPgriv96//dWgWW7iFCMPCmTUgsa5zH6R/mlUCNjNs+VHod8SCnsKhP46ivPgRG+7wdP5Vnv2C6b1n2aIBDJqqUP/cX+F+9+mpkIpF7pV19MLk1KSaiLgJQcSp9waUUtGuynhs5/IltqNcAMd/iJ3dCO7N9vcEp+TDEPz8R6bFiCRpFyGA0wOyj51pvI6Agz8tYpvheb3rBeGu7c3oulLp2JhAM90lqffBv3MAkURdOcGIOaEN3+YGfA/hn3Mo=", + "Tags": [ + { + "Key": "Env", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "Production" + }, + { + "Key": "Name", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "custodian-tester" + }, + { + "Key": "Owner", + "ResourceId": "i-0f0b98cda19cde0a0", + "ResourceType": "instance", + "Value": "robot" + }, + { + "Key": "Env", + "ResourceId": "i-0877d42a6928968d2", + "ResourceType": "instance", + "Value": "Production" + }, + { + "Key": "Name", + "ResourceId": "i-0877d42a6928968d2", + "ResourceType": "instance", + "Value": "custodian-tester" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_2.json b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_2.json new file mode 100644 index 00000000000..7a8a645ee72 --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_lookup_many/ec2.DescribeTags_2.json @@ -0,0 +1,14 @@ +{ + "status_code": 200, + "data": { + "Tags": [ + { + "Key": "Owner", + "ResourceId": "i-0877d42a6928968d2", + "ResourceType": "instance", + "Value": "robot" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ec2_augment_tags_short_circuit/ec2.DescribeInstances_1.json b/tests/data/placebo/test_ec2_augment_tags_short_circuit/ec2.DescribeInstances_1.json new file mode 100644 index 00000000000..c8cf6237c39 --- /dev/null +++ b/tests/data/placebo/test_ec2_augment_tags_short_circuit/ec2.DescribeInstances_1.json @@ -0,0 +1,199 @@ +{ + "status_code": 200, + "data": { + "Reservations": [ + { + "ReservationId": "r-0bf0c1883f8890732", + "OwnerId": "644160558196", + "Groups": [], + "Instances": [ + { + "Architecture": "x86_64", + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 20, + "microsecond": 0 + }, + "DeleteOnTermination": true, + "Status": "attached", + "VolumeId": "vol-07758c92d403d0fd0", + "EbsCardIndex": 0 + } + } + ], + "ClientToken": "terraform-mOXgODjveTJZr9d5ucYvZJTAVj", + "EbsOptimized": false, + "EnaSupport": true, + "Hypervisor": "xen", + "NetworkInterfaces": [ + { + "Attachment": { + "AttachTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "AttachmentId": "eni-attach-098cf2cc45e8d5b7c", + "DeleteOnTermination": true, + "DeviceIndex": 0, + "Status": "attached", + "NetworkCardIndex": 0 + }, + "Description": "", + "Groups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "Ipv6Addresses": [], + "MacAddress": "12:ff:e8:8e:01:a7", + "NetworkInterfaceId": "eni-0b119f9162c9c3ebf", + "OwnerId": "644160558196", + "PrivateIpAddress": "10.0.1.79", + "PrivateIpAddresses": [ + { + "Primary": true, + "PrivateIpAddress": "10.0.1.79" + } + ], + "SourceDestCheck": true, + "Status": "in-use", + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "InterfaceType": "interface", + "Operator": { + "Managed": false + } + } + ], + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "SecurityGroups": [ + { + "GroupId": "sg-091079d352b8ffdcf", + "GroupName": "default" + } + ], + "SourceDestCheck": true, + "Tags": [ + { + "Key": "Owner", + "Value": "robot" + }, + { + "Key": "Name", + "Value": "custodian-tester" + }, + { + "Key": "Env", + "Value": "Production" + } + ], + "VirtualizationType": "hvm", + "CpuOptions": { + "CoreCount": 1, + "ThreadsPerCore": 1 + }, + "CapacityReservationSpecification": { + "CapacityReservationPreference": "open" + }, + "HibernationOptions": { + "Configured": false + }, + "MetadataOptions": { + "State": "applied", + "HttpTokens": "optional", + "HttpPutResponseHopLimit": 1, + "HttpEndpoint": "enabled", + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "EnclaveOptions": { + "Enabled": false + }, + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "UsageOperationUpdateTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "PrivateDnsNameOptions": { + "HostnameType": "ip-name", + "EnableResourceNameDnsARecord": false, + "EnableResourceNameDnsAAAARecord": false + }, + "MaintenanceOptions": { + "AutoRecovery": "default", + "RebootMigration": "default" + }, + "CurrentInstanceBootMode": "legacy-bios", + "NetworkPerformanceOptions": { + "BandwidthWeighting": "default" + }, + "Operator": { + "Managed": false, + "HiddenByDefault": false + }, + "SecondaryInterfaces": [], + "InstanceId": "i-0f0b98cda19cde0a0", + "ImageId": "ami-0f8848d57eb4fe2d2", + "State": { + "Code": 16, + "Name": "running" + }, + "PrivateDnsName": "ip-10-0-1-79.ec2.internal", + "PublicDnsName": "", + "StateTransitionReason": "", + "AmiLaunchIndex": 0, + "ProductCodes": [], + "InstanceType": "t2.micro", + "LaunchTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 20, + "minute": 36, + "second": 19, + "microsecond": 0 + }, + "Placement": { + "AvailabilityZoneId": "use1-az2", + "GroupName": "", + "Tenancy": "default", + "AvailabilityZone": "us-east-1b" + }, + "Monitoring": { + "State": "disabled" + }, + "SubnetId": "subnet-0d517737a3ddc6342", + "VpcId": "vpc-0b03ecd013983bb42", + "PrivateIpAddress": "10.0.1.79" + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/ec2_augment_tags/ami.tf b/tests/terraform/ec2_augment_tags/ami.tf new file mode 100644 index 00000000000..9cb1726d17c --- /dev/null +++ b/tests/terraform/ec2_augment_tags/ami.tf @@ -0,0 +1,12 @@ +data "aws_ami" "amazon_linux" { + most_recent = true + + owners = ["amazon"] + + filter { + name = "name" + values = [ + "amzn2-ami-hvm-*-x86_64-gp2", + ] + } +} diff --git a/tests/terraform/ec2_augment_tags/main.tf b/tests/terraform/ec2_augment_tags/main.tf new file mode 100644 index 00000000000..41476169845 --- /dev/null +++ b/tests/terraform/ec2_augment_tags/main.tf @@ -0,0 +1,23 @@ +locals { + tags = { + Env = "Production" + Name = "custodian-tester" + Owner = "robot" + } +} + +# Two instances: one is enough for the by-id lookup, the second exercises the +# fall back to a region-wide filter (with the ceiling patched down in tests). +resource "aws_instance" "tagged_a" { + ami = data.aws_ami.amazon_linux.id + instance_type = "t2.micro" + subnet_id = aws_subnet.example.id + tags = local.tags +} + +resource "aws_instance" "tagged_b" { + ami = data.aws_ami.amazon_linux.id + instance_type = "t2.micro" + subnet_id = aws_subnet.example.id + tags = local.tags +} diff --git a/tests/terraform/ec2_augment_tags/network.tf b/tests/terraform/ec2_augment_tags/network.tf new file mode 100644 index 00000000000..3c95d7488b0 --- /dev/null +++ b/tests/terraform/ec2_augment_tags/network.tf @@ -0,0 +1,8 @@ +resource "aws_vpc" "example" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "example" { + vpc_id = aws_vpc.example.id + cidr_block = "10.0.1.0/24" +} diff --git a/tests/terraform/ec2_augment_tags/tf_resources.json b/tests/terraform/ec2_augment_tags/tf_resources.json new file mode 100644 index 00000000000..7fe860333bc --- /dev/null +++ b/tests/terraform/ec2_augment_tags/tf_resources.json @@ -0,0 +1,408 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_ami": { + "amazon_linux": { + "allow_unsafe_filter": null, + "architecture": "x86_64", + "arn": "arn:aws:ec2:us-east-1::image/ami-0f8848d57eb4fe2d2", + "block_device_mappings": [ + { + "device_name": "/dev/xvda", + "ebs": { + "delete_on_termination": "true", + "encrypted": "false", + "iops": "0", + "snapshot_id": "snap-04b0e344914d64b89", + "throughput": "0", + "volume_initialization_rate": "0", + "volume_size": "8", + "volume_type": "gp2" + }, + "no_device": "", + "virtual_name": "" + } + ], + "boot_mode": "", + "creation_date": "2026-09-29T19:49:49.000Z", + "deprecation_time": "2026-12-28T19:51:00.000Z", + "description": "Amazon Linux 2 AMI 2.0.20260930.0 x86_64 HVM gp2", + "ena_support": true, + "executable_users": null, + "filter": [ + { + "name": "name", + "values": [ + "amzn2-ami-hvm-*-x86_64-gp2" + ] + } + ], + "hypervisor": "xen", + "id": "ami-0f8848d57eb4fe2d2", + "image_id": "ami-0f8848d57eb4fe2d2", + "image_location": "amazon/amzn2-ami-hvm-2.0.20260930.0-x86_64-gp2", + "image_owner_alias": "amazon", + "image_type": "machine", + "imds_support": "", + "include_deprecated": false, + "kernel_id": "", + "last_launched_time": "", + "most_recent": true, + "name": "amzn2-ami-hvm-2.0.20260930.0-x86_64-gp2", + "name_regex": null, + "owner_id": "644160558196", + "owners": [ + "amazon" + ], + "platform": "", + "platform_details": "Linux/UNIX", + "product_codes": [], + "public": true, + "ramdisk_id": "", + "region": "us-east-1", + "root_device_name": "/dev/xvda", + "root_device_type": "ebs", + "root_snapshot_id": "snap-04b0e344914d64b89", + "sriov_net_support": "simple", + "state": "available", + "state_reason": { + "code": "UNSET", + "message": "UNSET" + }, + "tags": {}, + "timeouts": null, + "tpm_support": "", + "uefi_data": null, + "usage_operation": "RunInstances", + "virtualization_type": "hvm" + } + }, + "aws_instance": { + "tagged_a": { + "ami": "ami-0f8848d57eb4fe2d2", + "arn": "arn:aws:ec2:us-east-1:644160558196:instance/i-0f0b98cda19cde0a0", + "associate_public_ip_address": false, + "availability_zone": "us-east-1b", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0f0b98cda19cde0a0", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0b119f9162c9c3ebf" + } + ], + "primary_network_interface_id": "eni-0b119f9162c9c3ebf", + "private_dns": "ip-10-0-1-79.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.1.79", + "public_dns": "", + "public_ip": "", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 100, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-07758c92d403d0fd0", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-0d517737a3ddc6342", + "tags": { + "Env": "Production", + "Name": "custodian-tester", + "Owner": "robot" + }, + "tags_all": { + "Env": "Production", + "Name": "custodian-tester", + "Owner": "robot" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-091079d352b8ffdcf" + ] + }, + "tagged_b": { + "ami": "ami-0f8848d57eb4fe2d2", + "arn": "arn:aws:ec2:us-east-1:644160558196:instance/i-0877d42a6928968d2", + "associate_public_ip_address": false, + "availability_zone": "us-east-1b", + "capacity_reservation_specification": [ + { + "capacity_reservation_preference": "open", + "capacity_reservation_target": [] + } + ], + "cpu_options": [ + { + "amd_sev_snp": "", + "core_count": 1, + "nested_virtualization": "", + "threads_per_core": 1 + } + ], + "credit_specification": [ + { + "cpu_credits": "standard" + } + ], + "disable_api_stop": false, + "disable_api_termination": false, + "ebs_block_device": [], + "ebs_optimized": false, + "enable_primary_ipv6": null, + "enclave_options": [ + { + "enabled": false + } + ], + "ephemeral_block_device": [], + "force_destroy": false, + "get_password_data": false, + "hibernation": false, + "host_id": "", + "host_resource_group_arn": null, + "iam_instance_profile": "", + "id": "i-0877d42a6928968d2", + "instance_initiated_shutdown_behavior": "stop", + "instance_lifecycle": "", + "instance_market_options": [], + "instance_state": "running", + "instance_type": "t2.micro", + "ipv6_address_count": 0, + "ipv6_addresses": [], + "key_name": "", + "launch_template": [], + "maintenance_options": [ + { + "auto_recovery": "default" + } + ], + "metadata_options": [ + { + "http_endpoint": "enabled", + "http_protocol_ipv6": "disabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "instance_metadata_tags": "disabled" + } + ], + "monitoring": false, + "network_interface": [], + "outpost_arn": "", + "password_data": "", + "placement_group": "", + "placement_group_id": "", + "placement_partition_number": 0, + "primary_network_interface": [ + { + "delete_on_termination": true, + "network_interface_id": "eni-0707f5ab16555de2a" + } + ], + "primary_network_interface_id": "eni-0707f5ab16555de2a", + "private_dns": "ip-10-0-1-122.ec2.internal", + "private_dns_name_options": [ + { + "enable_resource_name_dns_a_record": false, + "enable_resource_name_dns_aaaa_record": false, + "hostname_type": "ip-name" + } + ], + "private_ip": "10.0.1.122", + "public_dns": "", + "public_ip": "", + "region": "us-east-1", + "root_block_device": [ + { + "delete_on_termination": true, + "device_name": "/dev/xvda", + "encrypted": false, + "iops": 100, + "kms_key_id": "", + "tags": {}, + "tags_all": {}, + "throughput": 0, + "volume_id": "vol-01a4a812d6e1e264f", + "volume_size": 8, + "volume_type": "gp2" + } + ], + "secondary_network_interface": [], + "secondary_private_ips": [], + "security_groups": [], + "source_dest_check": true, + "spot_instance_request_id": "", + "subnet_id": "subnet-0d517737a3ddc6342", + "tags": { + "Env": "Production", + "Name": "custodian-tester", + "Owner": "robot" + }, + "tags_all": { + "Env": "Production", + "Name": "custodian-tester", + "Owner": "robot" + }, + "tenancy": "default", + "timeouts": null, + "user_data": null, + "user_data_base64": null, + "user_data_replace_on_change": false, + "volume_tags": null, + "vpc_security_group_ids": [ + "sg-091079d352b8ffdcf" + ] + } + }, + "aws_subnet": { + "example": { + "arn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-0d517737a3ddc6342", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-1b", + "availability_zone_id": "use1-az2", + "cidr_block": "10.0.1.0/24", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-0d517737a3ddc6342", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_ipam_pool_id": null, + "ipv6_native": false, + "ipv6_netmask_length": null, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "644160558196", + "private_dns_hostname_type_on_launch": "ip-name", + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "timeouts": null, + "vpc_id": "vpc-0b03ecd013983bb42" + } + }, + "aws_vpc": { + "example": { + "arn": "arn:aws:ec2:us-east-1:644160558196:vpc/vpc-0b03ecd013983bb42", + "assign_generated_ipv6_cidr_block": false, + "cidr_block": "10.0.0.0/16", + "default_network_acl_id": "acl-00a4e20e103da96bc", + "default_route_table_id": "rtb-0b77f9f964c5751fc", + "default_security_group_id": "sg-091079d352b8ffdcf", + "dhcp_options_id": "dopt-0caef10e2e26f8bde", + "enable_dns_hostnames": false, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "id": "vpc-0b03ecd013983bb42", + "instance_tenancy": "default", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_network_border_group": "", + "ipv6_ipam_pool_id": "", + "ipv6_netmask_length": 0, + "main_route_table_id": "rtb-0b77f9f964c5751fc", + "owner_id": "644160558196", + "region": "us-east-1", + "tags": null, + "tags_all": {} + } + } + } +} \ No newline at end of file diff --git a/tests/test_ec2.py b/tests/test_ec2.py index 5c7a46ea3d9..d5e722d4de9 100644 --- a/tests/test_ec2.py +++ b/tests/test_ec2.py @@ -187,6 +187,147 @@ def test_ec2_network_location_terminated(self): self.assertEqual(len(resources), 0) +AUGMENT_REGION = 'us-east-1' +AUGMENT_TAG_COUNT = 3 +CLOUDTRAIL_MODE = {'type': 'cloudtrail', 'events': ['RunInstances']} + + +def _strip_instance_tags(session_factory) -> None: + """Simulate missing DescribeInstances tags in live and replay runs.""" + def strip_tags(parsed: dict, **kw) -> None: + for reservation in parsed['Reservations']: + for instance in reservation['Instances']: + instance.pop('Tags', None) + + utils.local_session(session_factory).events.register( + 'after-call.ec2.DescribeInstances', strip_tags) + + +def _capture_tag_filters(test, session_factory): + """Collect the Filters sent on each DescribeTags request. + + Placebo stores only responses and replays them regardless of request, so + hooking the request is the only way to assert which filter the back-fill + chose. It has to be before-parameter-build rather than before-call: + placebo answers before-call itself, and botocore stops at the first + handler that returns a response. + """ + sent = [] + utils.local_session(session_factory).events.register( + 'before-parameter-build.ec2.DescribeTags', + lambda params, **kw: sent.append(params['Filters'])) + return sent + + +def _augment_policy(test, session_factory, mode=None): + data = {'name': 'ec2-tags', 'resource': 'ec2'} + if mode is not None: + data['mode'] = mode + return test.load_policy( + data, + session_factory=session_factory, + config={'region': AUGMENT_REGION}, + ) + + +@terraform('ec2_augment_tags', scope='session') +def test_tag_augment_short_circuit(test, ec2_augment_tags): + """Tags in the describe response mean no describe_tags call. + + The flight data has no DescribeTags response on purpose: placebo raises + if one is requested. + """ + session_factory = test.replay_flight_data( + 'test_ec2_augment_tags_short_circuit', region=AUGMENT_REGION) + instance_id = ec2_augment_tags['aws_instance.tagged_a.id'] + + policy = _augment_policy(test, session_factory, mode=CLOUDTRAIL_MODE) + resources = policy.resource_manager.get_resources([instance_id]) + + test.assertEqual(len(resources), 1) + test.assertEqual( + len(resources[0]['Tags']), AUGMENT_TAG_COUNT, + 'Proof found of AWOL tags!') + + +@terraform('ec2_augment_tags', scope='session') +def test_tag_augment_skipped_for_instance_state(test, ec2_augment_tags): + """ec2-instance-state mode still skips the back-fill. + + The flight data has no DescribeTags response, so placebo raises if the + lookup is attempted. Tags stays absent, as it did before this change. + """ + session_factory = test.replay_flight_data( + 'test_ec2_augment_tags_instance_state', region=AUGMENT_REGION) + _strip_instance_tags(session_factory) + instance_id = ec2_augment_tags['aws_instance.tagged_a.id'] + + policy = _augment_policy( + test, session_factory, + mode={'type': 'ec2-instance-state', 'events': ['pending']}) + resources = policy.resource_manager.get_resources([instance_id]) + + test.assertEqual(len(resources), 1) + test.assertNotIn('Tags', resources[0]) + + +@terraform('ec2_augment_tags', scope='session') +@pytest.mark.parametrize( + 'mode', + [None, {'type': 'pull'}, CLOUDTRAIL_MODE], + ids=['no-mode', 'pull', 'cloudtrail'], +) +def test_tag_augment_lookup_by_id(test, ec2_augment_tags, mode): + """Up to the ceiling, tags are looked up by instance id. + + Parametrized over the modes that should reach the lookup; cloudtrail is + the one this change enables. + """ + session_factory = test.replay_flight_data( + 'test_ec2_augment_tags_lookup', region=AUGMENT_REGION) + instance_id = ec2_augment_tags['aws_instance.tagged_a.id'] + _strip_instance_tags(session_factory) + sent = _capture_tag_filters(test, session_factory) + + policy = _augment_policy(test, session_factory, mode=mode) + resources = policy.resource_manager.get_resources([instance_id]) + + test.assertEqual(len(resources), 1) + tags = {t['Key']: t['Value'] for t in resources[0]['Tags']} + test.assertEqual(len(tags), AUGMENT_TAG_COUNT) + test.assertEqual(tags['Env'], 'Production') + test.assertEqual( + sent, [[{'Name': 'resource-id', 'Values': [instance_id]}]]) + + +@terraform('ec2_augment_tags', scope='session') +def test_tag_augment_lookup_by_resource_type(test, ec2_augment_tags): + """The region-wide lookup recovers instance tags across multiple pages.""" + session_factory = test.replay_flight_data( + 'test_ec2_augment_tags_lookup_many', region=AUGMENT_REGION) + test.patch(ec2, 'EC2_TAG_AUGMENT_BY_INSTANCES_MAX', 1) + ids = [ec2_augment_tags['aws_instance.tagged_a.id'], + ec2_augment_tags['aws_instance.tagged_b.id']] + _strip_instance_tags(session_factory) + # Six instance tags require multiple pages with this page size. + utils.local_session(session_factory).events.register( + 'before-parameter-build.ec2.DescribeTags', + lambda params, **kw: params.update(MaxResults=5)) + sent = _capture_tag_filters(test, session_factory) + + policy = _augment_policy(test, session_factory) + resources = policy.resource_manager.get_resources(ids) + + test.assertEqual(sorted(r['InstanceId'] for r in resources), sorted(ids)) + for r in resources: + test.assertEqual( + {t['Key']: t['Value'] for t in r['Tags']}, + {'Env': 'Production', 'Name': 'custodian-tester', 'Owner': 'robot'}) + test.assertGreater(len(sent), 1) + for filters in sent: + test.assertEqual(filters, [{'Name': 'resource-type', 'Values': ['instance']}]) + + class TestTagAugmentation(BaseTest): def test_tag_augment_empty(self): From 556f808ad3907e575d7a046e058291310c748006 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 13 Oct 2026 11:07:52 -0600 Subject: [PATCH 36/65] gcp - project - scope metrics filter to one project at a time (#11153) --------- Co-authored-by: dreakj12 --- tools/c7n_gcp/c7n_gcp/filters/metrics.py | 43 ++- .../c7n_gcp/resources/resourcemanager.py | 87 ++++- ...emanager.googleapis.com-v1-projects_1.json | 33 ++ ...projects-cloud-custodian-timeSeries_1.json | 20 ++ ...emanager.googleapis.com-v1-projects_1.json | 33 ++ ...projects-cloud-custodian-timeSeries_1.json | 90 ++++++ ...emanager.googleapis.com-v1-projects_1.json | 33 ++ ...emanager.googleapis.com-v1-projects_2.json | 33 ++ ...projects-cloud-custodian-timeSeries_1.json | 46 +++ ...projects-cloud-custodian-timeSeries_2.json | 46 +++ tools/c7n_gcp/tests/test_resourcemanager.py | 296 +++++++++++++++++- 11 files changed, 743 insertions(+), 17 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_2.json diff --git a/tools/c7n_gcp/c7n_gcp/filters/metrics.py b/tools/c7n_gcp/c7n_gcp/filters/metrics.py index 64cf0ad8332..7d1ef241ac5 100644 --- a/tools/c7n_gcp/c7n_gcp/filters/metrics.py +++ b/tools/c7n_gcp/c7n_gcp/filters/metrics.py @@ -131,12 +131,11 @@ def validate(self): return self - def process(self, resources, event=None): + def initialize_metric(self): days = self.data.get('days', 14) duration = timedelta(days) self.metric = self.data['name'] - self.metric_key = self.data.get('metric-key') or self.manager.resource_type.metric_key self.aligner = self.data.get('aligner', 'ALIGN_NONE') self.reducer = self.data.get('reducer', 'REDUCE_NONE') self.group_by_fields = self.data.get('group-by-fields', []) @@ -154,24 +153,38 @@ def process(self, resources, event=None): self.filter = self.data.get('filter', '') self.c7n_metric_key = "%s.%s.%s" % (self.metric, self.aligner, self.reducer) + def get_query_params(self, query_filter): + return { + 'filter': query_filter, + 'interval_startTime': self.start.isoformat() + 'Z', + 'interval_endTime': self.end.isoformat() + 'Z', + 'aggregation_alignmentPeriod': self.period, + "aggregation_perSeriesAligner": self.aligner, + "aggregation_crossSeriesReducer": self.reducer, + "aggregation_groupByFields": self.group_by_fields, + 'view': 'FULL' + } + + def metric_project(self, resource=None): + return local_session(self.manager.session_factory).get_default_project() + + def process(self, resources, event=None): + self.initialize_metric() + self.metric_key = self.data.get('metric-key') or self.manager.resource_type.metric_key + session = local_session(self.manager.session_factory) client = session.client("monitoring", "v3", "projects.timeSeries") - project = session.get_default_project() + project = self.metric_project() time_series_data = [] for batched_filter in self.get_batched_query_filter(resources): - query_params = { - 'filter': batched_filter, - 'interval_startTime': self.start.isoformat() + 'Z', - 'interval_endTime': self.end.isoformat() + 'Z', - 'aggregation_alignmentPeriod': self.period, - "aggregation_perSeriesAligner": self.aligner, - "aggregation_crossSeriesReducer": self.reducer, - "aggregation_groupByFields": self.group_by_fields, - 'view': 'FULL' - } - metric_list = client.execute_query('list', - {'name': 'projects/' + project, **query_params}) + metric_list = client.execute_query( + 'list', + { + 'name': 'projects/' + project, + **self.get_query_params(batched_filter), + }, + ) time_series_data.extend(metric_list.get('timeSeries', [])) if not time_series_data: diff --git a/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py b/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py index 62f88a6fc9b..bd67e3c7d37 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py +++ b/tools/c7n_gcp/c7n_gcp/resources/resourcemanager.py @@ -3,14 +3,17 @@ import itertools from c7n_gcp.filters.iampolicy import IamPolicyFilter +from c7n_gcp.filters.metrics import ALIGNERS, REDUCERS, GCPMetricsFilter from c7n_gcp.actions import SetIamPolicy, MethodAction from c7n_gcp.provider import resources from c7n_gcp.query import QueryResourceManager, TypeInfo +from c7n.exceptions import PolicyExecutionError from c7n.resolver import ValuesFrom from c7n.utils import type_schema, local_session -from c7n.filters.core import ValueFilter, ListItemFilter +from c7n.filters.core import OPERATORS, ValueFilter, ListItemFilter +from c7n.filters.metrics import METRIC_WINDOW_ALIGNMENT from c7n.filters.missing import Missing from googleapiclient.errors import HttpError @@ -136,6 +139,88 @@ def get_resource_query(self): Project.filter_registry.register('missing', Missing) +@Project.filter_registry.register('metrics') +class GCPProjectMetricsFilter(GCPMetricsFilter): + """Filter projects by an aggregate Cloud Monitoring metric. + + The project supplies the Monitoring API scope, so this filter does not use + the resource join key required by the generic ``metrics`` filter. + + :example: + + .. code-block:: yaml + + policies: + - name: vertex-ai-project-monthly-token-usage + resource: gcp.project + filters: + - type: metrics + name: aiplatform.googleapis.com/publisher/online_serving/token_count + days: 30 + period-start: start-of-day + aligner: ALIGN_SUM + reducer: REDUCE_SUM + op: gt + value: 3000000 + """ + + schema = type_schema( + 'metrics', + **{ + 'name': {'type': 'string'}, + 'days': {'type': 'number'}, + 'period-start': {'type': 'string', 'enum': METRIC_WINDOW_ALIGNMENT}, + 'op': {'type': 'string', 'enum': list(OPERATORS.keys())}, + 'reducer': {'type': 'string', 'enum': REDUCERS}, + 'aligner': {'type': 'string', 'enum': ALIGNERS}, + 'value': {'type': 'number'}, + 'filter': {'type': 'string'}, + 'missing-value': {'type': 'number'}, + 'required': ('value', 'name', 'op'), + }, + ) + + def validate(self): + return self + + def metric_project(self, resource=None): + return resource['projectId'] + + def process(self, resources, event=None): + self.initialize_metric() + self.metric_key = None + + session = local_session(self.manager.session_factory) + client = session.client('monitoring', 'v3', 'projects.timeSeries') + query_filter = f'metric.type = "{self.metric}"' + if self.filter: + query_filter = f'{query_filter} AND ({self.filter})' + query_params = self.get_query_params(query_filter) + + for resource in resources: + time_series = [] + pages = client.execute_paged_query( + 'list', + { + 'name': f'projects/{self.metric_project(resource)}', + **query_params, + }, + ) + for page in pages: + time_series.extend(page.get('timeSeries', [])) + if len(time_series) > 1: + raise PolicyExecutionError( + 'project metric query returned more than one time series' + ) + if time_series: + resource_name = self.manager.resource_type.get_metric_resource_name( + resource, + ) + self.resource_metric_dict[resource_name] = time_series[0] + + return [resource for resource in resources if self.process_resource(resource)] + + @Project.filter_registry.register('iam-policy') class ProjectIamPolicyFilter(IamPolicyFilter): """ diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..27f2498a1b3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,33 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:17 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=338", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "315", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?filter=id%3Acloud-custodian&alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "693936050605", + "projectId": "cloud-custodian", + "lifecycleState": "ACTIVE", + "name": "cloud-custodian", + "createTime": "2026-07-02T19:11:33.870Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json new file mode 100644 index 00000000000..a7ae4ba812b --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter-missing-value/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json @@ -0,0 +1,20 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:18 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "18", + "-content-encoding": "gzip", + "content-location": "https://monitoring.googleapis.com/v3/projects/cloud-custodian/timeSeries?filter=metric.type+%3D+%22aiplatform.googleapis.com%2Fpublisher%2Fonline_serving%2Ftoken_count%22+AND+%28metric.labels.type+%3D+%22does-not-exist%22%29&interval.startTime=2026-10-08T20%3A29%3A17Z&interval.endTime=2026-10-09T20%3A29%3A17Z&aggregation.alignmentPeriod=86400.0s&aggregation.perSeriesAligner=ALIGN_SUM&aggregation.crossSeriesReducer=REDUCE_SUM&view=FULL&alt=json" + }, + "body": { + "unit": "1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..672172b59c4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,33 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:30:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=256", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "315", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?filter=id%3Acloud-custodian&alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "693936050605", + "projectId": "cloud-custodian", + "lifecycleState": "ACTIVE", + "name": "cloud-custodian", + "createTime": "2026-07-02T19:11:33.870Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json new file mode 100644 index 00000000000..4e4445a3d80 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter-multiple-series/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json @@ -0,0 +1,90 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:30:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1850", + "-content-encoding": "gzip", + "content-location": "https://monitoring.googleapis.com/v3/projects/cloud-custodian/timeSeries?filter=metric.type+%3D+%22aiplatform.googleapis.com%2Fpublisher%2Fonline_serving%2Ftoken_count%22&interval.startTime=2026-10-08T20%3A30%3A00Z&interval.endTime=2026-10-09T20%3A30%3A00Z&aggregation.alignmentPeriod=86400.0s&aggregation.perSeriesAligner=ALIGN_SUM&aggregation.crossSeriesReducer=REDUCE_NONE&view=FULL&alt=json" + }, + "body": { + "timeSeries": [ + { + "metric": { + "labels": { + "shared_request_type": "standard", + "request_type": "shared", + "source": "us-central1", + "type": "input" + }, + "type": "aiplatform.googleapis.com/publisher/online_serving/token_count" + }, + "resource": { + "type": "aiplatform.googleapis.com/PublisherModel", + "labels": { + "project_id": "cloud-custodian", + "location": "us-central1", + "publisher": "google", + "model_version_id": "", + "model_user_id": "gemini-2.5-flash" + } + }, + "metricKind": "DELTA", + "valueType": "INT64", + "points": [ + { + "interval": { + "startTime": "2026-10-08T20:30:00Z", + "endTime": "2026-10-09T20:30:00Z" + }, + "value": { + "int64Value": "6" + } + } + ] + }, + { + "metric": { + "labels": { + "shared_request_type": "standard", + "source": "us-central1", + "request_type": "shared", + "type": "output" + }, + "type": "aiplatform.googleapis.com/publisher/online_serving/token_count" + }, + "resource": { + "type": "aiplatform.googleapis.com/PublisherModel", + "labels": { + "model_user_id": "gemini-2.5-flash", + "project_id": "cloud-custodian", + "publisher": "google", + "model_version_id": "", + "location": "us-central1" + } + }, + "metricKind": "DELTA", + "valueType": "INT64", + "points": [ + { + "interval": { + "startTime": "2026-10-08T20:30:00Z", + "endTime": "2026-10-09T20:30:00Z" + }, + "value": { + "int64Value": "24" + } + } + ] + } + ], + "unit": "1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..1a785f3e7b1 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,33 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:15 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=292", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "315", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?filter=id%3Acloud-custodian&alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "693936050605", + "projectId": "cloud-custodian", + "lifecycleState": "ACTIVE", + "name": "cloud-custodian", + "createTime": "2026-07-02T19:11:33.870Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_2.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_2.json new file mode 100644 index 00000000000..08a76afa763 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-cloudresourcemanager.googleapis.com-v1-projects_2.json @@ -0,0 +1,33 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:16 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=285", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "315", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?filter=id%3Acloud-custodian&alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "693936050605", + "projectId": "cloud-custodian", + "lifecycleState": "ACTIVE", + "name": "cloud-custodian", + "createTime": "2026-07-02T19:11:33.870Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json new file mode 100644 index 00000000000..0f1411f1f0d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_1.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:16 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "620", + "-content-encoding": "gzip", + "content-location": "https://monitoring.googleapis.com/v3/projects/cloud-custodian/timeSeries?filter=metric.type+%3D+%22aiplatform.googleapis.com%2Fpublisher%2Fonline_serving%2Ftoken_count%22&interval.startTime=2026-10-08T20%3A29%3A15Z&interval.endTime=2026-10-09T20%3A29%3A15Z&aggregation.alignmentPeriod=86400.0s&aggregation.perSeriesAligner=ALIGN_SUM&aggregation.crossSeriesReducer=REDUCE_SUM&view=FULL&alt=json" + }, + "body": { + "timeSeries": [ + { + "metric": { + "type": "aiplatform.googleapis.com/publisher/online_serving/token_count" + }, + "resource": { + "type": "aiplatform.googleapis.com/PublisherModel", + "labels": { + "project_id": "cloud-custodian" + } + }, + "metricKind": "DELTA", + "valueType": "INT64", + "points": [ + { + "interval": { + "startTime": "2026-10-08T20:29:15Z", + "endTime": "2026-10-09T20:29:15Z" + }, + "value": { + "int64Value": "6" + } + } + ] + } + ], + "unit": "1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_2.json b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_2.json new file mode 100644 index 00000000000..1c1311541ec --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-metric-filter/get-monitoring.googleapis.com-v3-projects-cloud-custodian-timeSeries_2.json @@ -0,0 +1,46 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 09 Oct 2026 20:29:16 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "620", + "-content-encoding": "gzip", + "content-location": "https://monitoring.googleapis.com/v3/projects/cloud-custodian/timeSeries?filter=metric.type+%3D+%22aiplatform.googleapis.com%2Fpublisher%2Fonline_serving%2Ftoken_count%22&interval.startTime=2026-10-08T20%3A29%3A16Z&interval.endTime=2026-10-09T20%3A29%3A16Z&aggregation.alignmentPeriod=86400.0s&aggregation.perSeriesAligner=ALIGN_SUM&aggregation.crossSeriesReducer=REDUCE_SUM&view=FULL&alt=json" + }, + "body": { + "timeSeries": [ + { + "metric": { + "type": "aiplatform.googleapis.com/publisher/online_serving/token_count" + }, + "resource": { + "type": "aiplatform.googleapis.com/PublisherModel", + "labels": { + "project_id": "cloud-custodian" + } + }, + "metricKind": "DELTA", + "valueType": "INT64", + "points": [ + { + "interval": { + "startTime": "2026-10-08T20:29:16Z", + "endTime": "2026-10-09T20:29:16Z" + }, + "value": { + "int64Value": "6" + } + } + ] + } + ], + "unit": "1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_resourcemanager.py b/tools/c7n_gcp/tests/test_resourcemanager.py index 4c3365a0fab..7b2383290a2 100644 --- a/tools/c7n_gcp/tests/test_resourcemanager.py +++ b/tools/c7n_gcp/tests/test_resourcemanager.py @@ -1,11 +1,14 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import datetime import logging import os import sys import time from unittest import mock +import google.auth +from google.auth.transport.requests import AuthorizedSession import pytest from pytest_terraform import terraform @@ -16,7 +19,15 @@ from gcp_common import BaseTest -from c7n.exceptions import ResourceLimitExceeded +from c7n.exceptions import ( + PolicyExecutionError, + PolicyValidationError, + ResourceLimitExceeded, +) + + +TOKEN_COUNT_METRIC = 'aiplatform.googleapis.com/publisher/online_serving/token_count' +TOKEN_COUNT_ANNOTATION = f'{TOKEN_COUNT_METRIC}.ALIGN_SUM.REDUCE_SUM' ORG_ID = '999999999999' FOLDER_ID = '111111111111' @@ -299,6 +310,49 @@ def test_folder_iam_policy_filter_verb_arguments(self, mock_base_verb_arguments) class ProjectTest(BaseTest): + def generate_token_count_metric(self): + credentials, _ = google.auth.default( + scopes=['https://www.googleapis.com/auth/cloud-platform'] + ) + session = AuthorizedSession(credentials) + model = 'gemini-2.5-flash' + response = session.post( + 'https://us-central1-aiplatform.googleapis.com/v1/' + f'projects/{self.project_id}/locations/us-central1/' + f'publishers/google/models/{model}:generateContent', + json={ + 'contents': [ + { + 'role': 'user', + 'parts': [{'text': 'Reply with: metric recording test'}], + }, + ], + }, + ) + response.raise_for_status() + + deadline = time.time() + 300 + while True: + end = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0) + response = session.get( + f'https://monitoring.googleapis.com/v3/projects/' + f'{self.project_id}/timeSeries', + params={ + 'filter': f'metric.type = "{TOKEN_COUNT_METRIC}"', + 'interval.startTime': ( + end - datetime.timedelta(days=1) + ).isoformat(), + 'interval.endTime': end.isoformat(), + 'view': 'FULL', + }, + ) + response.raise_for_status() + if response.json().get('timeSeries'): + return + if time.time() >= deadline: + raise RuntimeError('timed out waiting for token-count metric') + time.sleep(10) + def test_project_get(self): factory = self.replay_flight_data( 'project-get-resource', project_id='cloud-custodian') @@ -316,6 +370,246 @@ def test_project_get(self): ], ) + def test_project_metric_filter_rejects_resource_grouping(self): + for field, value in ( + ('metric-key', 'resource.labels.project_id'), + ('group-by-fields', ['resource.labels.project_id']), + ): + with self.subTest(field=field): + with self.assertRaises(PolicyValidationError): + self.load_policy( + { + 'name': 'project-metric-invalid', + 'resource': 'gcp.project', + 'filters': [ + { + 'type': 'metrics', + 'name': 'example.googleapis.com/metric', + 'op': 'gt', + 'value': 0, + field: value, + }, + ], + }, + ) + + def test_project_metric_filter_rejects_paginated_series(self): + policy = self.load_policy( + { + 'name': 'project-metric-paginated', + 'resource': 'gcp.project', + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'days': 1, + 'aligner': 'ALIGN_SUM', + 'op': 'gt', + 'value': 0, + }, + ], + }, + ) + metric_filter = policy.resource_manager.filters[0] + series = { + 'metric': {'type': TOKEN_COUNT_METRIC}, + 'resource': { + 'type': 'aiplatform.googleapis.com/PublisherModel', + 'labels': {'project_id': 'resource-project'}, + }, + 'points': [ + { + 'interval': { + 'startTime': '2026-09-24T00:00:00Z', + 'endTime': '2026-09-25T00:00:00Z', + }, + 'value': {'int64Value': '1'}, + }, + ], + } + client = mock.Mock() + client.execute_query.return_value = { + 'timeSeries': [series], + 'nextPageToken': 'next-page', + } + client.execute_paged_query.return_value = iter( + [ + {'timeSeries': [series], 'nextPageToken': 'next-page'}, + {'timeSeries': [series]}, + ] + ) + session = mock.Mock() + session.client.return_value = client + + with mock.patch( + 'c7n_gcp.resources.resourcemanager.local_session', + return_value=session, + ): + with self.assertRaisesRegex( + PolicyExecutionError, + 'project metric query returned more than one time series', + ): + metric_filter.process([{'projectId': 'resource-project'}]) + + def test_project_metric_filter_uses_resource_project(self): + policy = self.load_policy( + { + 'name': 'project-metric-scope', + 'resource': 'gcp.project', + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'missing-value': 0, + 'op': 'eq', + 'value': 0, + }, + ], + }, + ) + metric_filter = policy.resource_manager.filters[0] + requests = [] + + class Client: + def execute_paged_query(self, verb, arguments): + requests.append((verb, arguments)) + yield {} + + class Session: + def client(self, service, version, component): + return Client() + + def get_default_project(self): + return 'default-project' + + with mock.patch( + 'c7n_gcp.resources.resourcemanager.local_session', + return_value=Session(), + ): + resources = metric_filter.process([{'projectId': 'resource-project'}]) + + self.assertEqual(len(resources), 1) + self.assertEqual(requests[0][1]['name'], 'projects/resource-project') + + def test_project_metric_filter(self): + project_id = self.project_id + session_factory = self.replay_flight_data( + 'project-metric-filter', project_id=project_id) + if self.recording: + self.generate_token_count_metric() + policy = self.load_policy( + { + 'name': 'project-metric', + 'resource': 'gcp.project', + 'query': [{'filter': f'id:{project_id}'}], + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'days': 1, + 'aligner': 'ALIGN_SUM', + 'reducer': 'REDUCE_SUM', + 'op': 'gt', + 'value': 0, + }, + ], + }, + session_factory=session_factory, + ) + + resources = policy.run() + + self.assertEqual(len(resources), 1) + metric = resources[0]['c7n.metrics'][TOKEN_COUNT_ANNOTATION] + self.assertGreater( + int(metric['points'][0]['value']['int64Value']), + 0, + ) + + nonmatching_policy = self.load_policy( + { + 'name': 'project-metric-nonmatching', + 'resource': 'gcp.project', + 'query': [{'filter': f'id:{project_id}'}], + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'days': 1, + 'aligner': 'ALIGN_SUM', + 'reducer': 'REDUCE_SUM', + 'op': 'gt', + 'value': 10**12, + }, + ], + }, + session_factory=session_factory, + ) + self.assertEqual(nonmatching_policy.run(), []) + + def test_project_metric_filter_missing_value(self): + project_id = self.project_id + session_factory = self.replay_flight_data( + 'project-metric-filter-missing-value', project_id=project_id) + policy = self.load_policy( + { + 'name': 'project-metric-missing-value', + 'resource': 'gcp.project', + 'query': [{'filter': f'id:{project_id}'}], + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'days': 1, + 'aligner': 'ALIGN_SUM', + 'reducer': 'REDUCE_SUM', + 'filter': 'metric.labels.type = "does-not-exist"', + 'missing-value': 0, + 'op': 'eq', + 'value': 0, + }, + ], + }, + session_factory=session_factory, + ) + + resources = policy.run() + + self.assertEqual(len(resources), 1) + self.assertIsNone(resources[0]['c7n.metrics'][TOKEN_COUNT_ANNOTATION]) + + def test_project_metric_filter_rejects_multiple_series(self): + project_id = self.project_id + session_factory = self.replay_flight_data( + 'project-metric-filter-multiple-series', project_id=project_id) + if self.recording: + self.generate_token_count_metric() + + policy = self.load_policy( + { + 'name': 'project-metric-multiple-series', + 'resource': 'gcp.project', + 'query': [{'filter': f'id:{project_id}'}], + 'filters': [ + { + 'type': 'metrics', + 'name': TOKEN_COUNT_METRIC, + 'days': 1, + 'aligner': 'ALIGN_SUM', + 'op': 'gt', + 'value': 0, + }, + ], + }, + session_factory=session_factory, + ) + + with self.assertRaisesRegex( + PolicyExecutionError, + 'project metric query returned more than one time series', + ): + policy.run() + @pytest.mark.skipif( sys.platform.startswith('win'), reason='windows file path fun') def test_propagate_tags(self): From 5380fefec87be700da72298f0c7d98fc28504da9 Mon Sep 17 00:00:00 2001 From: Jim Fulton Date: Tue, 13 Oct 2026 11:09:11 -0600 Subject: [PATCH 37/65] azure - add machine learning compute cluster resource (#11117) --------- Co-authored-by: Claude Opus 5 (1M context) --- .../machine_learning_compute_cluster.py | 357 +++++ .../c7n_azure/resources/resource_map.py | 1 + tools/c7n_azure/c7n_azure/utils.py | 39 + tools/c7n_azure/tests_azure/azure_common.py | 118 +- ...ning-compute-cluster-inactive-current.json | 876 +++++++++++++ ...rning-compute-cluster-inactive-recent.json | 772 +++++++++++ ...achine-learning-compute-cluster-query.json | 221 ++++ ...earning-compute-cluster-set-min-nodes.json | 1143 +++++++++++++++++ .../machine-learning-compute-cluster-jobs.sh | 206 +++ .../machine-learning-compute-cluster.json | 105 ++ .../tests_azure/test_azure_common.py | 210 +++ .../c7n_azure/tests_azure/test_azure_utils.py | 37 + .../test_machine_learning_compute_cluster.py | 1021 +++++++++++++++ 13 files changed, 5097 insertions(+), 9 deletions(-) create mode 100644 tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-current.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-recent.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-query.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-set-min-nodes.json create mode 100755 tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster-jobs.sh create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster.json create mode 100644 tools/c7n_azure/tests_azure/test_azure_common.py create mode 100644 tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_compute_cluster.py diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py new file mode 100644 index 00000000000..852d5687d93 --- /dev/null +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_compute_cluster.py @@ -0,0 +1,357 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import datetime +import re +from urllib.parse import urlsplit, urlunsplit + +import isodate +import requests + +from azure.core.exceptions import HttpResponseError +from azure.mgmt.machinelearningservices.models import ( + ClusterUpdateParameters, + ScaleSettings, + ScaleSettingsInformation, +) + +from c7n.exceptions import PolicyValidationError +from c7n.filters.core import Filter, type_schema +from c7n_azure import utils +from c7n_azure.actions.base import AzureBaseAction +from c7n_azure.provider import resources +from c7n_azure.resources.arm import ChildArmResourceManager +from c7n_azure.utils import ResourceIdParser + + +WORKSPACE_DISCOVERY_URL = 'c7n:WorkspaceDiscoveryUrl' +DURATION_UNITS = { + 'm': 'minutes', + 'h': 'hours', + 'd': 'days', + 'w': 'weeks', +} +# The Run History audience is not one of the cloud's ARM endpoints. +AML_AUDIENCES = { + 'AzureCloud': 'https://ml.azure.com', + 'AzureChinaCloud': 'https://ml.azure.cn', + 'AzureUSGovernment': 'https://ml.azure.us', +} +# Every Run History run status except terminal Completed/Failed/Canceled. +NONTERMINAL_STATUSES = ( + 'NotStarted', + 'Starting', + 'Provisioning', + 'Preparing', + 'Queued', + 'Running', + 'Finalizing', + 'CancelRequested', + 'NotResponding', + 'Unapproved', + 'Pausing', + 'Paused', +) + + +@resources.register('machine-learning-compute-cluster') +class MachineLearningComputeCluster(ChildArmResourceManager): + """Azure Machine Learning AmlCompute cluster resource. + + :example: + + Find allocated AmlCompute clusters. + + .. code-block:: yaml + + policies: + - name: machine-learning-compute-clusters-allocated + resource: azure.machine-learning-compute-cluster + filters: + - type: value + key: properties.properties.currentNodeCount + op: gt + value: 0 + """ + + class resource_type(ChildArmResourceManager.resource_type): + doc_groups = ['AI + Machine Learning'] + service = 'azure.mgmt.machinelearningservices' + client = 'MachineLearningServicesMgmtClient' + enum_spec = ('compute', 'list', None) + parent_manager_name = 'machine-learning-workspace' + resource_type = 'Microsoft.MachineLearningServices/workspaces/computes' + raise_on_exception = False + default_report_fields = ( + 'name', + 'resourceGroup', + '"c7n:parent-id"', + 'properties.properties.currentNodeCount', + 'properties.properties.nodeStateCounts.runningNodeCount', + 'properties.properties.scaleSettings.minNodeCount', + 'properties.properties.scaleSettings.maxNodeCount', + ) + + @classmethod + def extra_args(cls, parent_resource): + return { + 'resource_group_name': parent_resource['resourceGroup'], + 'workspace_name': parent_resource['name'], + } + + def enumerate_resources(self, parent_resource, type_info, vault_url=None, **params): + resources = super().enumerate_resources( + parent_resource, + type_info, + vault_url=vault_url, + **params, + ) + clusters = [ + resource for resource in resources + if resource['properties']['computeType'] == 'AmlCompute' + ] + discovery_url = parent_resource['properties'].get('discoveryUrl') + for cluster in clusters: + cluster[WORKSPACE_DISCOVERY_URL] = discovery_url + return clusters + + +@MachineLearningComputeCluster.filter_registry.register('inactive') +class InactiveFilter(Filter): + """Find clusters with no current or recent job activity. + + :example: + + .. code-block:: yaml + + policies: + - name: machine-learning-compute-clusters-inactive + resource: azure.machine-learning-compute-cluster + filters: + - type: value + key: properties.properties.currentNodeCount + op: gt + value: 0 + - type: inactive + since: 1d + """ + + schema = type_schema( + 'inactive', + required=['since'], + since={ + 'type': 'string', + 'pattern': r'^[1-9][0-9]*[mhdw]$', + }, + ) + + def validate(self): + # Policy-level schema validation does not enforce this filter's + # `required`, so a missing `since` only fails here. + if 'since' not in self.data: + raise PolicyValidationError("inactive filter requires 'since'") + return self + + def process( + self, + resources: list[dict], + event=None, + ) -> list[dict]: + match = re.fullmatch(r'([1-9][0-9]*)([mhdw])', self.data['since']) + amount, unit = match.groups() + cutoff = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta( + **{DURATION_UNITS[unit]: int(amount)} + ) + + workspaces = {} + for resource in resources: + node_counts = resource['properties'].get('properties', {}).get( + 'nodeStateCounts', + {}, + ) or {} + if (node_counts.get('runningNodeCount') or 0) > 0: + continue + workspaces.setdefault(resource['c7n:parent-id'], []).append(resource) + + inactive = [] + for workspace_resources in workspaces.values(): + workspace = workspace_resources[0] + if not workspace.get(WORKSPACE_DISCOVERY_URL): + self.log.warning( + 'Workspace %s has no discovery URL; skipping inactivity check', + workspace['c7n:parent-id'], + ) + continue + try: + targets = self._get_active_targets(workspace, cutoff) + except requests.RequestException as error: + self.log.warning( + 'Run History query failed for workspace %s: %s', + workspace['c7n:parent-id'], + error, + ) + continue + inactive.extend( + resource for resource in workspace_resources + if resource['name'].lower() not in targets + ) + return inactive + + def _query_history(self, url: str, body: dict) -> list[dict]: + azure_session = self.manager.get_session() + audience = AML_AUDIENCES[azure_session.cloud_endpoints.name] + session = utils.requests_session( + azure_session, + token_scope=f'{audience}/.default', + max_retries=3, + allowed_methods=('POST',), + ) + headers = {'Content-Type': 'application/json'} + params = {'api-version': '2023-10-01'} + values = [] + request_body = dict(body) + + while True: + response = session.post( + url, + headers=headers, + params=params, + json=request_body, + timeout=30, + ) + response.raise_for_status() + page = response.json() + if not isinstance(page, dict) or not isinstance(page.get('value'), list): + raise TypeError('Run History response value must be a list') + values.extend(page['value']) + continuation_token = page.get('continuationToken') + if not continuation_token: + return values + request_body = dict(body) + request_body['continuationToken'] = continuation_token + + def _get_experiments(self, workspace: dict) -> list[str]: + endpoint = self._get_history_endpoint(workspace) + workspace_id = workspace['c7n:parent-id'] + url = f'{endpoint}/history/v1.0{workspace_id}/experiments:query' + # Archiving an experiment does not end its runs, so a non-terminal run + # can live in an experiment archived long before the cutoff. + experiments = self._query_history(url, {'viewType': 'All'}) + + experiment_ids = [] + seen = set() + for experiment in experiments: + experiment_id = experiment.get('experimentId') + if experiment_id and experiment_id not in seen: + experiment_ids.append(experiment_id) + seen.add(experiment_id) + return experiment_ids + + def _get_active_targets( + self, + workspace: dict, + cutoff: datetime.datetime, + ) -> set[str]: + endpoint = self._get_history_endpoint(workspace) + workspace_id = workspace['c7n:parent-id'] + cutoff_text = self._format_cutoff(cutoff) + status_filter = ' or '.join( + f"status eq '{status}'" for status in NONTERMINAL_STATUSES + ) + targets = set() + + for experiment_id in self._get_experiments(workspace): + url = ( + f'{endpoint}/history/v1.0{workspace_id}/experimentids/' + f'{experiment_id}/runs:query' + ) + runs = self._query_history(url, {'filter': status_filter}) + runs.extend(self._query_history( + url, + {'filter': f'endTimeUtc ge {cutoff_text}'}, + )) + for run in runs: + target = run.get('target') + if isinstance(target, str) and target: + targets.add(target.lower()) + return targets + + @staticmethod + def _get_history_endpoint(workspace: dict) -> str: + discovery_url = workspace[WORKSPACE_DISCOVERY_URL] + parts = urlsplit(discovery_url) + if not parts.path.endswith('/discovery'): + raise ValueError('Workspace discovery URL must end in /discovery') + path = parts.path[:-len('/discovery')] + return urlunsplit((parts.scheme, parts.netloc, path, '', '')) + + @staticmethod + def _format_cutoff(cutoff: datetime.datetime) -> str: + return cutoff.astimezone(datetime.timezone.utc).isoformat().replace( + '+00:00', + 'Z', + ) + + +@MachineLearningComputeCluster.action_registry.register('set-min-nodes') +class SetMinNodesAction(AzureBaseAction): + """Set a cluster's minimum node count. + + This preserves the cluster and its maximum and idle scale-down settings. + + :example: + + Set the minimum node count to zero. + + .. code-block:: yaml + + policies: + - name: machine-learning-compute-clusters-set-min-nodes + resource: azure.machine-learning-compute-cluster + actions: + - type: set-min-nodes + value: 0 + """ + + schema = type_schema( + 'set-min-nodes', + required=['value'], + value={ + 'type': 'integer', + 'minimum': 0, + }, + ) + + def _prepare_processing(self): + self.client = self.manager.get_client() + + def _process_resource(self, resource): + scale_settings = resource['properties']['properties']['scaleSettings'] + idle_time = scale_settings.get('nodeIdleTimeBeforeScaleDown') + parameters = ClusterUpdateParameters( + properties=ScaleSettingsInformation( + scale_settings=ScaleSettings( + max_node_count=scale_settings['maxNodeCount'], + min_node_count=self.data['value'], + node_idle_time_before_scale_down=( + isodate.parse_duration(idle_time) + if idle_time else None + ), + ), + ), + ) + try: + self.client.compute.begin_update( + resource_group_name=ResourceIdParser.get_resource_group( + resource['id'] + ), + workspace_name=ResourceIdParser.get_resource_name( + resource['c7n:parent-id'] + ), + compute_name=resource['name'], + parameters=parameters, + ) + except HttpResponseError as error: + # The service can accept updates with 202, which this SDK rejects. + if error.response is None or error.response.status_code != 202: + raise diff --git a/tools/c7n_azure/c7n_azure/resources/resource_map.py b/tools/c7n_azure/c7n_azure/resources/resource_map.py index bed7605b624..7dd4f3fddbf 100644 --- a/tools/c7n_azure/c7n_azure/resources/resource_map.py +++ b/tools/c7n_azure/c7n_azure/resources/resource_map.py @@ -68,6 +68,7 @@ "azure.loadbalancer": "c7n_azure.resources.load_balancer.LoadBalancer", "azure.logic-app-workflow": "c7n_azure.resources.logic_app.LogicAppWorkflow", "azure.machine-learning-workspace": "c7n_azure.resources.machine_learning.MachineLearningWorkspace", # noqa + "azure.machine-learning-compute-cluster": "c7n_azure.resources.machine_learning_compute_cluster.MachineLearningComputeCluster", # noqa "azure.machine-learning-data-container": "c7n_azure.resources.machine_learning.MachineLearningDataContainer", # noqa "azure.machine-learning-job": "c7n_azure.resources.machine_learning_job.MachineLearningJob", "azure.machine-learning-model-version": "c7n_azure.resources.machine_learning_model_version.MachineLearningModelVersion", # noqa diff --git a/tools/c7n_azure/c7n_azure/utils.py b/tools/c7n_azure/c7n_azure/utils.py index 8d5c5023fe8..7e30eeb7f25 100644 --- a/tools/c7n_azure/c7n_azure/utils.py +++ b/tools/c7n_azure/c7n_azure/utils.py @@ -9,6 +9,7 @@ import random import re import time +import typing import uuid from concurrent.futures import as_completed from functools import lru_cache @@ -22,6 +23,8 @@ from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id from netaddr import IPNetwork, IPRange, IPSet +import requests +import urllib3.util.retry from c7n_azure import constants @@ -608,6 +611,42 @@ def get_keyvault_auth_endpoint(cloud_endpoints): return 'https://{0}'.format(cloud_endpoints.suffixes.keyvault_dns[1:]) +def requests_session( + azure_session: typing.Any, + token_scope: typing.Optional[str] = None, + max_retries: int = 0, + status_forcelist: tuple[int, ...] = (429, 503), + allowed_methods: tuple[str, ...] = ('GET',), + **retry_args: typing.Any, +) -> requests.Session: + """Create an Azure-authenticated HTTP session with optional retries.""" + azure_session._initialize_session() + token_scope = token_scope or ( + f"{azure_session.resource_endpoint.rstrip('/')}/.default" + ) + token = azure_session.credentials.get_token(token_scope) + + session = requests.Session() + session.headers['Authorization'] = f'Bearer {token.token}' + + if max_retries: + retry_options = { + 'total': max_retries, + 'status_forcelist': status_forcelist, + 'allowed_methods': allowed_methods, + 'respect_retry_after_header': True, + 'raise_on_status': False, + } + retry_options.update(retry_args) + retry = urllib3.util.retry.Retry(**retry_options) + session.mount( + 'https://', + requests.adapters.HTTPAdapter(max_retries=retry), + ) + + return session + + # This function is a workaround for Azure KeyVault objects that lack # standard serialization method. # These objects store variables with an underscore prefix, so we strip it. diff --git a/tools/c7n_azure/tests_azure/azure_common.py b/tools/c7n_azure/tests_azure/azure_common.py index 255e81a979e..a65c50c825c 100644 --- a/tools/c7n_azure/tests_azure/azure_common.py +++ b/tools/c7n_azure/tests_azure/azure_common.py @@ -9,6 +9,7 @@ import re from time import sleep from unittest.mock import patch +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit import azure.core.polling from msrest.pipeline import ClientRawResponse @@ -115,6 +116,7 @@ class AzureVCRBaseTest(VCRTestCase): TEST_DATE = None cassette_name = None + recording_resource_group = None FILTERED_HEADERS = ['authorization', 'accept-encoding', @@ -233,6 +235,9 @@ def _request_callback(self, request): """Modify requests before saving""" request.uri = AzureVCRBaseTest._replace_subscription_id(request.uri) request.uri = AzureVCRBaseTest._replace_tenant_id(request.uri) + request.uri = AzureVCRBaseTest._replace_async_operation_signature( + request.uri + ) if request.body: request.body = b'mock_body' @@ -243,6 +248,18 @@ def _request_callback(self, request): if re.match('https://login.microsoftonline.com/([^/]+)', request.uri): return None + # vcrpy also runs this callback over incoming playback requests, where + # dropping one means "no match" and falls through to a live call. + resource_group = self.recording_resource_group + if ( + resource_group + and not self.is_playback() + and '/resourcegroups/' in request.uri.lower() + and f'/resourcegroups/{resource_group}/'.lower() + not in request.uri.lower() + ): + return None + return request def _response_callback(self, response): @@ -258,13 +275,17 @@ def _response_callback(self, response): response['headers'].items() if k.lower() not in self.FILTERED_HEADERS} - # Headers such as location and azure-asyncoperation carry the - # subscription id, and are not covered by the body scrubbing below. - response['headers'] = { - k: [self._replace_subscription_id(self._replace_tenant_id(value)) - for value in v] - for (k, v) in response['headers'].items() - } + # Headers such as location and azure-asyncoperation can carry IDs + # and signed operation parameters not covered by body scrubbing. + for header, values in response['headers'].items(): + response['headers'][header] = [ + AzureVCRBaseTest._replace_async_operation_signature( + AzureVCRBaseTest._replace_tenant_id( + AzureVCRBaseTest._replace_subscription_id(value) + ) + ) + for value in values + ] if not response['body'].get('string'): return response @@ -280,6 +301,7 @@ def _response_callback(self, response): body = AzureVCRBaseTest._replace_subscription_id(body) body = AzureVCRBaseTest._replace_storage_keys(body) body = AzureVCRBaseTest._replace_instrumentation_key(body) + body = AzureVCRBaseTest._replace_user_email(body) try: response['body']['data'] = json.loads(body) @@ -289,14 +311,39 @@ def _response_callback(self, response): # Replace some API responses entirely response = AzureVCRBaseTest._response_substitutions(response) + response = self._scope_recorded_resource_group(response) return response + def _scope_recorded_resource_group(self, response): + resource_group = self.recording_resource_group + data = response['body']['data'] + if not resource_group or not isinstance(data, dict): + return response + + resources = data.get('value') + if not isinstance(resources, list) or not resources: + return response + if not all( + isinstance(resource, dict) + and '/resourceGroups/' in resource.get('id', '') + for resource in resources + ): + return response + + marker = f'/resourceGroups/{resource_group}/'.lower() + data['value'] = [ + resource for resource in resources + if marker in resource['id'].lower() + ] + return response + @staticmethod def _response_substitutions(response): data = response['body']['data'] if isinstance(data, dict): + AzureVCRBaseTest._remove_run_history_metadata(data) # Replace service tag responses if data.get('type', '') == 'Microsoft.Network/serviceTags': response['body']['data'] = SERVICE_TAG_RESPONSE @@ -331,6 +378,49 @@ def _response_substitutions(response): return response + @staticmethod + def _remove_run_history_metadata(data): + values = data.get('value') + if not isinstance(values, list): + return + + for run in values: + if not isinstance(run, dict) or not ( + 'runId' in run or 'experimentId' in run + ): + continue + run.pop('createdBy', None) + run.pop('lastModifiedBy', None) + run.pop('userId', None) + properties = run.get('properties') + if isinstance(properties, dict): + for key in list(properties): + if ( + key.startswith('mlflow.source.git.') + or key.startswith('azureml.git.') + ): + properties.pop(key) + + @staticmethod + def _replace_async_operation_signature(value): + parts = urlsplit(value) + path = parts.path.lower() + if not any( + segment in path + for segment in ('operationsstatus/', 'operationstatuses/', + 'operationresults/') + ): + return value + + query = parse_qsl(parts.query, keep_blank_values=True) + if not any(key in ('c', 's', 'h') for key, _ in query): + return value + + return urlunsplit(parts._replace(query=urlencode([ + (key, 'redacted' if key in ('c', 's', 'h') else item) + for key, item in query + ]))) + @staticmethod def _replace_subscription_id(s): prefixes = [r'(\\?/|%2F)?subscriptions(\\?/|%2F)', @@ -358,13 +448,20 @@ def _replace_subscription_id(s): @staticmethod def _replace_tenant_id(s): prefixes = ['(/|%2F)graph.windows.net(/|%2F)', - '"(t|T)enantId":\\s*"'] + '"(t|T)enantId":\\s*"', + '"userTenantId":\\s*"', + 'https://sts.windows.net/'] regex = r"(?P(%s))" \ r"[\da-zA-Z]{8}-([\da-zA-Z]{4}-){3}[\da-zA-Z]{12}" \ % '|'.join(['(%s)' % p for p in prefixes]) return re.sub(regex, r"\g" + DEFAULT_TENANT_ID, s) + @staticmethod + def _replace_user_email(s): + regex = r'(?P"(createdBy|lastModifiedBy|userId)":\s*")[^"]+@[^"]+' + return re.sub(regex, r'\guser@example.com', s) + @staticmethod def _replace_storage_keys(s): # All usages of storage keys have the word "key" somewhere @@ -376,7 +473,10 @@ def _replace_storage_keys(s): @staticmethod def _replace_instrumentation_key(s): - prefixes = ['"InstrumentationKey":\\s*"'] + prefixes = [ + '"InstrumentationKey":\\s*"', + '"sdkTelemetryAppInsightsKey":\\s*"', + ] regex = r"(?P(%s))" \ r"[\da-zA-Z]{8}-([\da-zA-Z]{4}-){3}[\da-zA-Z]{12}" \ diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-current.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-current.json new file mode 100644 index 00000000000..49d523fd0fd --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-current.json @@ -0,0 +1,876 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "x-msedge-ref": [ + "Ref A: 763B9BDF4A794DEC990836685CA739F2 Ref B: CYS013050703019 Ref C: 2026-09-28T20:44:38Z" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:37 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "cache-control": [ + "no-cache" + ], + "content-length": [ + "12926" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-response-type": [ + "standard" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "cache-control": [ + "no-cache" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.054" + ], + "x-msedge-ref": [ + "Ref A: D6741B3177474AA6893CCD0AB8903C64 Ref B: CYS013050704023 Ref C: 2026-09-28T20:44:41Z" + ], + "content-length": [ + "1864" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:40 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:40:09.5155815+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 1, + "targetNodeCount": 1, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 1, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:42:04.849+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.101" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:42 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "90cfed89-2e87-4392-bbb0-8ffa7a17c0dd" + ], + "content-length": [ + "1003" + ] + }, + "body": { + "data": { + "value": [ + { + "experimentId": "b8116c22-d63e-4d98-956b-40d2c42a5b0d", + "name": "cctest-recent-archived", + "description": null, + "createdUtc": "2026-09-28T20:40:16.9750834+00:00", + "tags": {}, + "archivedTime": "2026-09-28T20:44:02.4799205+00:00", + "retainForLifetimeOfWorkspace": false, + "artifactLocation": null + }, + { + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "name": "cctest-recent-active", + "description": null, + "createdUtc": "2026-09-28T20:40:23.9294339+00:00", + "tags": {}, + "archivedTime": null, + "retainForLifetimeOfWorkspace": false, + "artifactLocation": null + }, + { + "experimentId": "b8e983e1-68ba-44fe-8380-cb29c29837b8", + "name": "cctest-current", + "description": null, + "createdUtc": "2026-09-28T20:44:26.1122156+00:00", + "tags": {}, + "archivedTime": null, + "retainForLifetimeOfWorkspace": false, + "artifactLocation": null + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8116c22-d63e-4d98-956b-40d2c42a5b0d/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.066" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:42 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "8bef4b75-f27d-4aa7-8cd1-4d8b40201330" + ], + "content-length": [ + "17" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8116c22-d63e-4d98-956b-40d2c42a5b0d/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.061" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:43 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "df67f473-299c-47e8-94f1-85b2622ba24e" + ], + "content-length": [ + "17" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/c8c24090-4e39-4dd6-8c7e-18a8854a3f18/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.059" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:44 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "52170115-b13f-42b7-bfd6-ff97cdda068a" + ], + "content-length": [ + "17" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/c8c24090-4e39-4dd6-8c7e-18a8854a3f18/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.087" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:45 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "3ecb7ce8-252d-43de-ae31-e4f81a118b8c" + ], + "content-length": [ + "10887" + ] + }, + "body": { + "data": { + "value": [ + { + "runNumber": 1790628023, + "rootRunId": "cctest-recent-pipeline", + "createdUtc": "2026-09-28T20:40:23.9468176+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 8, + "statusRevision": 4, + "runUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "parentRunUuid": null, + "rootRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "lastStartTimeUtc": null, + "currentComputeTime": null, + "computeDuration": "00:03:26.9488884", + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:43:54.7339366+00:00", + "duration": "00:03:26.9488884", + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "cctest-recent-pipeline", + "parentRunId": null, + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "status": "Completed", + "startTimeUtc": "2026-09-28T20:40:27.8909734+00:00", + "endTimeUtc": "2026-09-28T20:43:54.8398618+00:00", + "scheduleId": null, + "displayName": "cctest-recent-pipeline", + "name": null, + "dataContainerId": "dcid.cctest-recent-pipeline", + "description": null, + "hidden": false, + "runType": "azureml.PipelineRun", + "runTypeV2": { + "orchestrator": "Pipeline", + "traits": [ + "azureml.PipelineRun", + "MFE", + "HTTP", + "DevPlatv2" + ], + "attribution": null, + "computeType": null + }, + "properties": { + "azureml.DevPlatv2": "true", + "azureml.DatasetAccessMode": "Asset", + "azureml.runsource": "azureml.PipelineRun", + "runSource": "MFE", + "runType": "HTTP", + "azureml.parameters": "{}", + "azureml.continue_on_step_failure": "True", + "azureml.continue_on_failed_optional_input": "True", + "azureml.enforceRerun": "False", + "azureml.defaultComputeName": "cctest-ml-cluster", + "azureml.defaultDataStoreName": "workspaceblobstore", + "azureml.pipelineComponent": "pipelinerun", + "azureml.pipelines.stages": "{\"Initialization\":null,\"Execution\":{\"StartTime\":\"2026-09-28T20:40:28.4013219+00:00\",\"EndTime\":\"2026-09-28T20:43:54.6670158+00:00\",\"Status\":\"Finished\"}}" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/studioservice/api/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/PipelineRuns/cctest-recent-pipeline/cancel" + }, + "scriptName": null, + "target": null, + "uniqueChildRunComputeTargets": [ + "cctest-ml-cluster" + ], + "tags": {}, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/studioservice/api/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/PipelineRuns/cctest-recent-pipeline/cancel", + "completeUri": null, + "diagnosticsUri": null, + "virtualWorkspaceStorageArmId": null, + "computeRequest": null, + "compute": null, + "retainForLifetimeOfWorkspace": false, + "queueingInfo": null, + "inputs": null, + "outputs": null + }, + { + "runNumber": 1790628033, + "rootRunId": "cctest-recent-pipeline", + "createdUtc": "2026-09-28T20:40:33.3150864+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 13, + "statusRevision": 6, + "runUuid": "b2e2b4e0-f506-4f42-9c61-3f037bc4aebd", + "parentRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "rootRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "lastStartTimeUtc": null, + "currentComputeTime": null, + "computeDuration": "00:00:09.9355203", + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:43:27.7910824+00:00", + "duration": "00:00:09.9355203", + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "f9e4ab16-2018-40ac-a67a-7e3790431342", + "parentRunId": "cctest-recent-pipeline", + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "status": "Completed", + "startTimeUtc": "2026-09-28T20:43:42.7129786+00:00", + "endTimeUtc": "2026-09-28T20:43:52.6484989+00:00", + "scheduleId": null, + "displayName": "child", + "name": "azureml_anonymous", + "dataContainerId": "dcid.f9e4ab16-2018-40ac-a67a-7e3790431342", + "description": null, + "hidden": false, + "runType": "azureml.StepRun", + "runTypeV2": { + "orchestrator": "Execution", + "traits": [ + "azureml.StepRun", + "scriptRun", + "remote", + "AlmostCommonRuntime", + "OsType:Linux", + "CommonRuntime" + ], + "attribution": "Aether", + "computeType": "AmlcTrain" + }, + "properties": { + "azureml.DevPlatv2": "true", + "azureml.DatasetAccessMode": "Asset", + "StepType": "PythonScriptStep", + "ComputeTargetType": "AmlCompute", + "azureml.moduleid": "a1892efa-7350-4d06-abee-9df10e31dad4", + "azureml.moduleFamilyId": "0", + "azureml.moduleName": "azureml_anonymous", + "azureml.moduleVersion": "1", + "azureml.runsource": "azureml.StepRun", + "azureml.nodeid": "1664266f", + "azureml.pipelinerunid": "cctest-recent-pipeline", + "azureml.pipeline": "cctest-recent-pipeline", + "azureml.rootpipelinerunid": "cctest-recent-pipeline", + "azureml.pipelineComponent": "masterescloud", + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctest-ml-cluster", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/studioservice/apiv2/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/runs/f9e4ab16-2018-40ac-a67a-7e3790431342/Cancel", + "Diagnose": "" + }, + "scriptName": null, + "target": "cctest-ml-cluster", + "uniqueChildRunComputeTargets": [], + "tags": { + "azureml.nodeid": "1664266f", + "azureml.pipeline": "cctest-recent-pipeline", + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/studioservice/apiv2/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/runs/f9e4ab16-2018-40ac-a67a-7e3790431342/Cancel", + "completeUri": null, + "diagnosticsUri": "", + "virtualWorkspaceStorageArmId": null, + "computeRequest": { + "nodeCount": 1, + "gpuCount": 0 + }, + "compute": { + "target": "cctest-ml-cluster", + "targetType": "amlcompute", + "vmSize": null, + "instanceType": null, + "instanceCount": 1, + "gpuCount": 0, + "priority": null, + "fineGrainPriority": null, + "region": null, + "armId": null, + "properties": null + }, + "retainForLifetimeOfWorkspace": null, + "queueingInfo": null, + "inputs": null, + "outputs": null + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8e983e1-68ba-44fe-8380-cb29c29837b8/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.087" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:45 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "068218af-6544-45c8-8bbb-f102b4483bda" + ], + "content-length": [ + "5330" + ] + }, + "body": { + "data": { + "value": [ + { + "runNumber": 1790628266, + "rootRunId": "cctest-current-standalone", + "createdUtc": "2026-09-28T20:44:26.1397904+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 8, + "statusRevision": 2, + "runUuid": "217c6427-c139-4ca8-af2d-34400e7caccd", + "parentRunUuid": null, + "rootRunUuid": "217c6427-c139-4ca8-af2d-34400e7caccd", + "lastStartTimeUtc": null, + "currentComputeTime": "00:00:00", + "computeDuration": null, + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:44:33.2530946+00:00", + "duration": null, + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "cctest-current-standalone", + "parentRunId": null, + "experimentId": "b8e983e1-68ba-44fe-8380-cb29c29837b8", + "status": "Queued", + "startTimeUtc": null, + "endTimeUtc": null, + "scheduleId": null, + "displayName": "cctest-current-standalone", + "name": null, + "dataContainerId": "dcid.cctest-current-standalone", + "description": null, + "hidden": false, + "runType": "azureml.scriptrun", + "runTypeV2": { + "orchestrator": "Execution", + "traits": [ + "scriptRun", + "remote", + "AlmostCommonRuntime", + "OsType:Linux", + "CommonRuntime" + ], + "attribution": "azureml.scriptrun", + "computeType": "AmlcTrain" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctest-ml-cluster", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/execution/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments/cctest-current/runId/cctest-current-standalone/cancel", + "Diagnose": "" + }, + "scriptName": null, + "target": "cctest-ml-cluster", + "uniqueChildRunComputeTargets": [], + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":1}" + }, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/execution/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments/cctest-current/runId/cctest-current-standalone/cancel", + "completeUri": null, + "diagnosticsUri": "", + "virtualWorkspaceStorageArmId": null, + "computeRequest": { + "nodeCount": 1, + "gpuCount": 0 + }, + "compute": { + "target": "cctest-ml-cluster", + "targetType": "amlcompute", + "vmSize": null, + "instanceType": null, + "instanceCount": 1, + "gpuCount": 0, + "priority": null, + "fineGrainPriority": null, + "region": null, + "armId": null, + "properties": null + }, + "retainForLifetimeOfWorkspace": false, + "queueingInfo": { + "code": "Submitted", + "message": "This job has been submitted to the compute.", + "lastRefreshTimestamp": "2026-09-28T20:44:32.7598983+00:00" + }, + "inputs": null, + "outputs": null + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8e983e1-68ba-44fe-8380-cb29c29837b8/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-request-time": [ + "0.059" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:46 GMT" + ], + "x-ms-response-type": [ + "standard" + ], + "mise-correlation-id": [ + "b1c980f7-3e5a-4c9c-a716-8b618980ec2c" + ], + "content-length": [ + "17" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-recent.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-recent.json new file mode 100644 index 00000000000..12833ef12c8 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-inactive-recent.json @@ -0,0 +1,772 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-msedge-ref": [ + "Ref A: BB4F785F68AF49C68C3DC400D89CD5AE Ref B: CYS013050703023 Ref C: 2026-09-28T20:44:09Z" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:08 GMT" + ], + "cache-control": [ + "no-cache" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "content-length": [ + "12926" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 2584E32C3C4F47789557DD95FE0E06C6 Ref B: CYS013050703023 Ref C: 2026-09-28T20:44:12Z" + ], + "x-request-time": [ + "0.051" + ], + "x-ms-response-type": [ + "standard" + ], + "content-length": [ + "1864" + ], + "date": [ + "Mon, 28 Sep 2026 20:44:11 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "cache-control": [ + "no-cache" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:40:09.5155815+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 1, + "targetNodeCount": 1, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 1, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:42:04.849+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Mon, 28 Sep 2026 20:44:13 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.033" + ], + "mise-correlation-id": [ + "8a095ca8-f1ed-4115-a1ad-63ffe06f55e1" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "690" + ] + }, + "body": { + "data": { + "value": [ + { + "experimentId": "b8116c22-d63e-4d98-956b-40d2c42a5b0d", + "name": "cctest-recent-archived", + "description": null, + "createdUtc": "2026-09-28T20:40:16.9750834+00:00", + "tags": {}, + "archivedTime": "2026-09-28T20:44:02.4799205+00:00", + "retainForLifetimeOfWorkspace": false, + "artifactLocation": null + }, + { + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "name": "cctest-recent-active", + "description": null, + "createdUtc": "2026-09-28T20:40:23.9294339+00:00", + "tags": {}, + "archivedTime": null, + "retainForLifetimeOfWorkspace": false, + "artifactLocation": null + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8116c22-d63e-4d98-956b-40d2c42a5b0d/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Mon, 28 Sep 2026 20:44:14 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.155" + ], + "mise-correlation-id": [ + "d1041509-6721-43c8-bc0f-ad49672a42bf" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-length": [ + "17" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/b8116c22-d63e-4d98-956b-40d2c42a5b0d/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Mon, 28 Sep 2026 20:44:14 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.076" + ], + "mise-correlation-id": [ + "1f1f1c22-a216-4a3a-a28f-5fea28c71f29" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "5259" + ] + }, + "body": { + "data": { + "value": [ + { + "runNumber": 1790628016, + "rootRunId": "cctest-recent-standalone", + "createdUtc": "2026-09-28T20:40:16.9907207+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 13, + "statusRevision": 5, + "runUuid": "ab8a4334-0722-4ab0-8094-d8758d49b102", + "parentRunUuid": null, + "rootRunUuid": "ab8a4334-0722-4ab0-8094-d8758d49b102", + "lastStartTimeUtc": null, + "currentComputeTime": null, + "computeDuration": "00:00:46.2871114", + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:40:24.4782653+00:00", + "duration": "00:00:46.2871114", + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "cctest-recent-standalone", + "parentRunId": null, + "experimentId": "b8116c22-d63e-4d98-956b-40d2c42a5b0d", + "status": "Completed", + "startTimeUtc": "2026-09-28T20:42:40.9246304+00:00", + "endTimeUtc": "2026-09-28T20:43:27.2117418+00:00", + "scheduleId": null, + "displayName": "cctest-recent-standalone", + "name": null, + "dataContainerId": "dcid.cctest-recent-standalone", + "description": null, + "hidden": true, + "runType": "azureml.scriptrun", + "runTypeV2": { + "orchestrator": "Execution", + "traits": [ + "scriptRun", + "remote", + "AlmostCommonRuntime", + "OsType:Linux", + "CommonRuntime" + ], + "attribution": "azureml.scriptrun", + "computeType": "AmlcTrain" + }, + "properties": { + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctest-ml-cluster", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/execution/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments/cctest-recent-archived/runId/cctest-recent-standalone/cancel", + "Diagnose": "" + }, + "scriptName": null, + "target": "cctest-ml-cluster", + "uniqueChildRunComputeTargets": [], + "tags": { + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/execution/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experiments/cctest-recent-archived/runId/cctest-recent-standalone/cancel", + "completeUri": null, + "diagnosticsUri": "", + "virtualWorkspaceStorageArmId": null, + "computeRequest": { + "nodeCount": 1, + "gpuCount": 0 + }, + "compute": { + "target": "cctest-ml-cluster", + "targetType": "amlcompute", + "vmSize": null, + "instanceType": null, + "instanceCount": 1, + "gpuCount": 0, + "priority": null, + "fineGrainPriority": null, + "region": null, + "armId": null, + "properties": null + }, + "retainForLifetimeOfWorkspace": false, + "queueingInfo": null, + "inputs": null, + "outputs": null + } + ] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/c8c24090-4e39-4dd6-8c7e-18a8854a3f18/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Mon, 28 Sep 2026 20:44:15 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.534" + ], + "mise-correlation-id": [ + "c5961639-42c6-488c-a81d-2175bc32d448" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-length": [ + "17" + ], + "content-type": [ + "application/json; charset=utf-8" + ] + }, + "body": { + "data": { + "value": [] + } + } + } + }, + { + "request": { + "method": "POST", + "uri": "https://southcentralus.api.azureml.ms/history/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/experimentids/c8c24090-4e39-4dd6-8c7e-18a8854a3f18/runs:query?api-version=2023-10-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "date": [ + "Mon, 28 Sep 2026 20:44:16 GMT" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "x-request-time": [ + "0.100" + ], + "mise-correlation-id": [ + "38b253fe-7182-4072-b0e7-116362a854c6" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "10887" + ] + }, + "body": { + "data": { + "value": [ + { + "runNumber": 1790628023, + "rootRunId": "cctest-recent-pipeline", + "createdUtc": "2026-09-28T20:40:23.9468176+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 8, + "statusRevision": 4, + "runUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "parentRunUuid": null, + "rootRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "lastStartTimeUtc": null, + "currentComputeTime": null, + "computeDuration": "00:03:26.9488884", + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:43:54.7339366+00:00", + "duration": "00:03:26.9488884", + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "cctest-recent-pipeline", + "parentRunId": null, + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "status": "Completed", + "startTimeUtc": "2026-09-28T20:40:27.8909734+00:00", + "endTimeUtc": "2026-09-28T20:43:54.8398618+00:00", + "scheduleId": null, + "displayName": "cctest-recent-pipeline", + "name": null, + "dataContainerId": "dcid.cctest-recent-pipeline", + "description": null, + "hidden": false, + "runType": "azureml.PipelineRun", + "runTypeV2": { + "orchestrator": "Pipeline", + "traits": [ + "azureml.PipelineRun", + "MFE", + "HTTP", + "DevPlatv2" + ], + "attribution": null, + "computeType": null + }, + "properties": { + "azureml.DevPlatv2": "true", + "azureml.DatasetAccessMode": "Asset", + "azureml.runsource": "azureml.PipelineRun", + "runSource": "MFE", + "runType": "HTTP", + "azureml.parameters": "{}", + "azureml.continue_on_step_failure": "True", + "azureml.continue_on_failed_optional_input": "True", + "azureml.enforceRerun": "False", + "azureml.defaultComputeName": "cctest-ml-cluster", + "azureml.defaultDataStoreName": "workspaceblobstore", + "azureml.pipelineComponent": "pipelinerun", + "azureml.pipelines.stages": "{\"Initialization\":null,\"Execution\":{\"StartTime\":\"2026-09-28T20:40:28.4013219+00:00\",\"EndTime\":\"2026-09-28T20:43:54.6670158+00:00\",\"Status\":\"Finished\"}}" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/studioservice/api/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/PipelineRuns/cctest-recent-pipeline/cancel" + }, + "scriptName": null, + "target": null, + "uniqueChildRunComputeTargets": [ + "cctest-ml-cluster" + ], + "tags": {}, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/studioservice/api/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/PipelineRuns/cctest-recent-pipeline/cancel", + "completeUri": null, + "diagnosticsUri": null, + "virtualWorkspaceStorageArmId": null, + "computeRequest": null, + "compute": null, + "retainForLifetimeOfWorkspace": false, + "queueingInfo": null, + "inputs": null, + "outputs": null + }, + { + "runNumber": 1790628033, + "rootRunId": "cctest-recent-pipeline", + "createdUtc": "2026-09-28T20:40:33.3150864+00:00", + "token": null, + "tokenExpiryTimeUtc": null, + "error": null, + "warnings": null, + "revision": 13, + "statusRevision": 6, + "runUuid": "b2e2b4e0-f506-4f42-9c61-3f037bc4aebd", + "parentRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "rootRunUuid": "eca8e228-14f6-4ecf-a57a-9f10f7c609a7", + "lastStartTimeUtc": null, + "currentComputeTime": null, + "computeDuration": "00:00:09.9355203", + "effectiveStartTimeUtc": null, + "lastModifiedUtc": "2026-09-28T20:43:27.7910824+00:00", + "duration": "00:00:09.9355203", + "cancelationReason": null, + "currentAttemptId": 1, + "deletionState": null, + "runId": "f9e4ab16-2018-40ac-a67a-7e3790431342", + "parentRunId": "cctest-recent-pipeline", + "experimentId": "c8c24090-4e39-4dd6-8c7e-18a8854a3f18", + "status": "Completed", + "startTimeUtc": "2026-09-28T20:43:42.7129786+00:00", + "endTimeUtc": "2026-09-28T20:43:52.6484989+00:00", + "scheduleId": null, + "displayName": "child", + "name": "azureml_anonymous", + "dataContainerId": "dcid.f9e4ab16-2018-40ac-a67a-7e3790431342", + "description": null, + "hidden": false, + "runType": "azureml.StepRun", + "runTypeV2": { + "orchestrator": "Execution", + "traits": [ + "azureml.StepRun", + "scriptRun", + "remote", + "AlmostCommonRuntime", + "OsType:Linux", + "CommonRuntime" + ], + "attribution": "Aether", + "computeType": "AmlcTrain" + }, + "properties": { + "azureml.DevPlatv2": "true", + "azureml.DatasetAccessMode": "Asset", + "StepType": "PythonScriptStep", + "ComputeTargetType": "AmlCompute", + "azureml.moduleid": "a1892efa-7350-4d06-abee-9df10e31dad4", + "azureml.moduleFamilyId": "0", + "azureml.moduleName": "azureml_anonymous", + "azureml.moduleVersion": "1", + "azureml.runsource": "azureml.StepRun", + "azureml.nodeid": "1664266f", + "azureml.pipelinerunid": "cctest-recent-pipeline", + "azureml.pipeline": "cctest-recent-pipeline", + "azureml.rootpipelinerunid": "cctest-recent-pipeline", + "azureml.pipelineComponent": "masterescloud", + "_azureml.ComputeTargetType": "amlctrain", + "_azureml.ClusterName": "cctest-ml-cluster", + "ProcessInfoFile": "azureml-logs/process_info.json", + "ProcessStatusFile": "azureml-logs/process_status.json" + }, + "parameters": {}, + "actionUris": { + "Cancel": "https://southcentralus.api.azureml.ms/studioservice/apiv2/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/runs/f9e4ab16-2018-40ac-a67a-7e3790431342/Cancel", + "Diagnose": "" + }, + "scriptName": null, + "target": "cctest-ml-cluster", + "uniqueChildRunComputeTargets": [], + "tags": { + "azureml.nodeid": "1664266f", + "azureml.pipeline": "cctest-recent-pipeline", + "_aml_system_ComputeTargetStatus": "{\"AllocationState\":\"steady\",\"PreparingNodeCount\":0,\"RunningNodeCount\":0,\"CurrentNodeCount\":0}" + }, + "settings": {}, + "services": {}, + "inputDatasets": [], + "outputDatasets": [], + "runDefinition": null, + "jobSpecification": null, + "primaryMetricName": null, + "createdFrom": null, + "cancelUri": "https://southcentralus.api.azureml.ms/studioservice/apiv2/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/workspaces/cctestml898871372560/runs/f9e4ab16-2018-40ac-a67a-7e3790431342/Cancel", + "completeUri": null, + "diagnosticsUri": "", + "virtualWorkspaceStorageArmId": null, + "computeRequest": { + "nodeCount": 1, + "gpuCount": 0 + }, + "compute": { + "target": "cctest-ml-cluster", + "targetType": "amlcompute", + "vmSize": null, + "instanceType": null, + "instanceCount": 1, + "gpuCount": 0, + "priority": null, + "fineGrainPriority": null, + "region": null, + "armId": null, + "properties": null + }, + "retainForLifetimeOfWorkspace": null, + "queueingInfo": null, + "inputs": null, + "outputs": null + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-query.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-query.json new file mode 100644 index 00000000000..a8e13ff17c0 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-query.json @@ -0,0 +1,221 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "cache-control": [ + "no-cache" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: E3134DDE540845DC84ADBAE4677C7E12 Ref B: CYS013050704051 Ref C: 2026-09-28T20:39:30Z" + ], + "content-length": [ + "12926" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:30 GMT" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1864" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:43 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: F18D99272CD64134A7DDC1C433945C78 Ref B: CYS013050703047 Ref C: 2026-09-28T20:39:43Z" + ], + "x-request-time": [ + "0.073" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:14.5378582+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-set-min-nodes.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-set-min-nodes.json new file mode 100644 index 00000000000..c374dc6fb66 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningComputeClusterTest.machine-learning-compute-cluster-set-min-nodes.json @@ -0,0 +1,1143 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "cache-control": [ + "no-cache" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 9C2F0CF25C6A40B39AE5F22264362719 Ref B: CYS013050701053 Ref C: 2026-09-28T20:39:45Z" + ], + "content-length": [ + "12926" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:45 GMT" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1864" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:48 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 74E71C480AF340EE9D0655A4538CC2DE Ref B: CYS013050703049 Ref C: 2026-09-28T20:39:48Z" + ], + "x-request-time": [ + "0.074" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:14.5378582+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "cache-control": [ + "no-cache" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 9CEB0FA1C7B14563989AE2AF0E8B9F61 Ref B: CYS013050701037 Ref C: 2026-09-28T20:39:48Z" + ], + "content-length": [ + "12926" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:48 GMT" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1864" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:51 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: B42FE5C3410544209A2F68FBC0C04AAE Ref B: CYS013050704053 Ref C: 2026-09-28T20:39:51Z" + ], + "x-request-time": [ + "0.051" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:14.5378582+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster?api-version=2023-04-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "Accepted" + }, + "headers": { + "content-length": [ + "1475" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:51 GMT" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 9F26C24BFFFD45D48E1DE4432CDB3A62 Ref B: CYS013050704027 Ref C: 2026-09-28T20:39:52Z" + ], + "x-request-time": [ + "0.141" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/locations/southcentralus/computeOperationsStatus/6194b58b-6016-4ea9-b698-326bc2af53ac?api-version=2023-04-01&service=new&t=639262247924281143&c=redacted&s=redacted&h=redacted" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:52.3044548+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.BatchAI/workspaces/cctestml898871372560/clusters/cctest-ml-cluster", + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Updating", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "STANDARD_DS2_V2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 1, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": null, + "targetNodeCount": null, + "nodeStateCounts": null, + "allocationState": null, + "errors": null, + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false + } + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1646" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:53 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: CC48E14E3EC6459CBF630FB46483C575 Ref B: CYS013050702019 Ref C: 2026-09-28T20:39:53Z" + ], + "x-request-time": [ + "0.094" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:52.3044548+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Updating", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 1, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1647" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:58 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 9F324A64D40E4EADB51A2E021557CB63 Ref B: CYS013050702027 Ref C: 2026-09-28T20:39:58Z" + ], + "x-request-time": [ + "0.210" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:58.6137151+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 1, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "cache-control": [ + "no-cache" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 2892800354A64B53A69A89CC051E81F0 Ref B: CYS013050702027 Ref C: 2026-09-28T20:39:59Z" + ], + "content-length": [ + "12926" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:39:59 GMT" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "name": "cctestml898871372560", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Storage/storageAccounts/cctestmlst898871372560", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.Keyvault/vaults/cctestmlkv898871372560", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.insights/components/cctestmlai898871372560", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-28T20:38:44.2714557Z", + "notebookInfo": { + "resourceId": "6039dc9fa1cb446abe8ca5dc8dcc8299", + "fqdn": "ml-cctestml-southcentralus-d0150ca1-0c13-4f91-9c20-60d6c28b26bc.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "d0150ca1-0c13-4f91-9c20-60d6c28b26bc", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "d658c321-7e68-4c7d-bfab-c932b24d467c", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-28T20:38:44.2714557Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-28T20:38:44.2714557Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1864" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:40:01 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 092D32DAE7A4400DBB99FE9A3BE1B620 Ref B: CYS013050701035 Ref C: 2026-09-28T20:40:02Z" + ], + "x-request-time": [ + "0.060" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:39:58.6137151+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 1, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + ] + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster?api-version=2023-04-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 202, + "message": "Accepted" + }, + "headers": { + "content-length": [ + "1475" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:40:02 GMT" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: F1B34345C1254B99820D7EEFBA0E1ABD Ref B: CYS013050701025 Ref C: 2026-09-28T20:40:02Z" + ], + "x-request-time": [ + "0.134" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/locations/southcentralus/computeOperationsStatus/3c705ea8-5a21-43f0-84cd-14ba1d70b2ad?api-version=2023-04-01&service=new&t=639262248033119654&c=redacted&s=redacted&h=redacted" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:40:03.2055756+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.BatchAI/workspaces/cctestml898871372560/clusters/cctest-ml-cluster", + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Updating", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "STANDARD_DS2_V2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": null, + "targetNodeCount": null, + "nodeStateCounts": null, + "allocationState": null, + "errors": null, + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false + } + } + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "1646" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Mon, 28 Sep 2026 20:40:03 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 2D08EB90F04D41DB8F0A7342BA080CA2 Ref B: CYS013050704037 Ref C: 2026-09-28T20:40:03Z" + ], + "x-request-time": [ + "0.079" + ], + "x-ms-response-type": [ + "standard" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-01" + ] + }, + "body": { + "data": { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-cluster/providers/Microsoft.MachineLearningServices/workspaces/cctestml898871372560/computes/cctest-ml-cluster", + "name": "cctest-ml-cluster", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-09-28T20:39:03.5706968+00:00", + "modifiedOn": "2026-09-28T20:40:03.2055756+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "AmlCompute", + "computeLocation": "southcentralus", + "provisioningState": "Updating", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "maxNodeCount": 1, + "minNodeCount": 0, + "nodeIdleTimeBeforeScaleDown": "PT2M" + }, + "subnet": null, + "currentNodeCount": 0, + "targetNodeCount": 0, + "nodeStateCounts": { + "preparingNodeCount": 0, + "runningNodeCount": 0, + "idleNodeCount": 0, + "unusableNodeCount": 0, + "leavingNodeCount": 0, + "preemptedNodeCount": 0 + }, + "allocationState": "Steady", + "allocationStateTransitionTime": "2026-09-28T20:39:13.648+00:00", + "errors": null, + "remoteLoginPortPublicAccess": "Enabled", + "osType": "Linux", + "virtualMachineImage": null, + "isolatedNetwork": false, + "enableBatchPrivateLink": false, + "propertyBag": {} + } + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster-jobs.sh b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster-jobs.sh new file mode 100755 index 00000000000..19e218b93b8 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster-jobs.sh @@ -0,0 +1,206 @@ +#!/bin/bash +set -euo pipefail + +resource_group="test_machine-learning-compute-cluster" +cluster_name="cctest-ml-cluster" +current_job="cctest-current-standalone" +recent_job="cctest-recent-standalone" +pipeline_job="cctest-recent-pipeline" + +workspace_name() { + az ml workspace list \ + --resource-group "$resource_group" \ + --query '[0].name' \ + --output tsv +} + +archive_experiment() { + local experiment_name="$1" + local subscription_id + local discovery_url + local history_url + local experiment_id + subscription_id=$(az account show --query id --output tsv) + discovery_url=$(az ml workspace show \ + --resource-group "$resource_group" \ + --name "$workspace" \ + --query discovery_url \ + --output tsv) + history_url="${discovery_url%/discovery}/history/v1.0/subscriptions" + history_url+="/$subscription_id/resourceGroups/$resource_group" + history_url+="/providers/Microsoft.MachineLearningServices" + history_url+="/workspaces/$workspace" + experiment_id=$(az rest \ + --method get \ + --resource https://ml.azure.com \ + --url "$history_url/experiments/$experiment_name?api-version=2023-10-01" \ + --query experimentId \ + --output tsv) + az rest \ + --method patch \ + --resource https://ml.azure.com \ + --url "$history_url/experimentids/$experiment_id?api-version=2023-10-01" \ + --body '{"archive": true}' \ + --output none +} + +job_status() { + az ml job show \ + --resource-group "$resource_group" \ + --workspace-name "$workspace" \ + --name "$1" \ + --query status \ + --output tsv +} + +wait_for_status() { + local job_name="$1" + shift + local status + for _ in {1..180}; do + status=$(job_status "$job_name") + for expected in "$@"; do + if [[ "$status" == "$expected" ]]; then + echo "$job_name reached $status" + return 0 + fi + done + if [[ "$status" =~ ^(Failed|Canceled|NotResponding)$ ]]; then + echo "$job_name unexpectedly reached $status" >&2 + return 1 + fi + sleep 5 + done + echo "Timed out waiting for $job_name; last status was $status" >&2 + return 1 +} + +wait_for_zero_running_nodes() { + local compute_url + local running_nodes + compute_url="https://management.azure.com/subscriptions/{subscriptionId}" + compute_url+="/resourceGroups/$resource_group" + compute_url+="/providers/Microsoft.MachineLearningServices" + compute_url+="/workspaces/$workspace/computes/$cluster_name" + compute_url+="?api-version=2023-04-01" + for _ in {1..90}; do + running_nodes=$(az rest \ + --method get \ + --url "$compute_url" \ + --query properties.properties.nodeStateCounts.runningNodeCount \ + --output tsv) + if [[ "$running_nodes" == "0" ]]; then + echo "$cluster_name reached zero running nodes" + return 0 + fi + sleep 10 + done + echo "$cluster_name did not reach zero running nodes" >&2 + return 1 +} + +create_current_job() { + cat >"$temp_directory/current.yml" <"$temp_directory/recent.yml" <"$temp_directory/pipeline.yml" </dev/null || true) + if [[ "$status" =~ ^(NotStarted|Starting|Provisioning|Preparing|Queued|Running|Finalizing)$ ]]; then + az ml job cancel \ + --resource-group "$resource_group" \ + --workspace-name "$workspace" \ + --name "$job_name" \ + --output none + fi +} + +usage() { + echo "Usage: $0 recent|current|cleanup" >&2 + exit 2 +} + +[[ $# -eq 1 ]] || usage +workspace=$(workspace_name) +[[ -n "$workspace" ]] || { + echo "No workspace exists in $resource_group" >&2 + exit 1 +} +temp_directory=$(mktemp -d) +trap 'rm -rf "$temp_directory"' EXIT + +case "$1" in + recent) + create_recent_jobs + ;; + current) + create_current_job + ;; + cleanup) + cancel_job "$current_job" + cancel_job "$recent_job" + cancel_job "$pipeline_job" + ;; + *) + usage + ;; +esac diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster.json b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster.json new file mode 100644 index 00000000000..403a5c233e3 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-cluster.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + } + }, + "variables": { + "workspaceName": "[concat('cctestml', parameters('uniqueId'))]", + "clusterName": "cctest-ml-cluster", + "storageName": "[concat('cctestmlst', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestmlkv', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestmlai', parameters('uniqueId'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-10-01", + "name": "[variables('keyVaultName')]", + "location": "[resourceGroup().location]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [], + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[resourceGroup().location]", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", + "publicNetworkAccess": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('clusterName'))]", + "location": "[resourceGroup().location]", + "properties": { + "computeType": "AmlCompute", + "properties": { + "vmSize": "Standard_DS2_v2", + "vmPriority": "Dedicated", + "scaleSettings": { + "minNodeCount": 0, + "maxNodeCount": 1, + "nodeIdleTimeBeforeScaleDown": "PT2M" + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/test_azure_common.py b/tools/c7n_azure/tests_azure/test_azure_common.py new file mode 100644 index 00000000000..d7ac24b1e87 --- /dev/null +++ b/tools/c7n_azure/tests_azure/test_azure_common.py @@ -0,0 +1,210 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import Mock, patch + +from .azure_common import ( + AzureVCRBaseTest, + BaseTest, + DEFAULT_INSTRUMENTATION_KEY, +) + + +class AzureCommonTest(BaseTest): + + recording_resource_group = 'test-recording-resource-group' + parent_id = ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/' + 'resourceGroups/test-rg/providers/Microsoft.MachineLearningServices/' + 'workspaces/test-workspace' + ) + + def test_recording_sanitizer_scopes_arm_resources(self): + test_resource = { + 'id': ( + f"{self.parent_id.replace('test-rg', self.recording_resource_group)}" + '/computes/test-cluster' + ), + } + unrelated_resource = { + 'id': self.parent_id.replace( + 'test-rg', + 'private-resource-group', + ), + } + response = { + 'body': { + 'data': { + 'value': [unrelated_resource, test_resource], + }, + }, + } + + self._scope_recorded_resource_group(response) + + self.assertEqual([test_resource], response['body']['data']['value']) + + def test_recording_sanitizer_omits_unrelated_arm_requests(self): + unrelated_request = Mock( + uri=( + 'https://management.azure.com/subscriptions/test/' + 'resourceGroups/private-resource-group/resources' + ), + body=None, + headers={}, + ) + test_request = Mock( + uri=( + 'https://management.azure.com/subscriptions/test/' + f'resourceGroups/{self.recording_resource_group}/resources' + ), + body=None, + headers={}, + ) + + # This unit test never records, so no cassette exists and playback is disabled. + self.assertFalse(self.is_playback()) + self.assertIsNone(self._request_callback(unrelated_request)) + self.assertIs(test_request, self._request_callback(test_request)) + + def test_recording_sanitizer_removes_run_identity_and_git_metadata(self): + run = { + 'runId': 'test-run', + 'createdBy': {'userName': 'Test User'}, + 'lastModifiedBy': {'userPuId': 'private-id'}, + 'userId': 'private-object-id', + 'properties': { + 'mlflow.source.git.repoURL': 'private-repository', + 'mlflow.source.git.branch': 'private-branch', + 'mlflow.source.git.commit': 'private-commit', + 'azureml.git.dirty': 'True', + 'ComputeTargetType': 'AmlCompute', + }, + } + response = {'body': {'data': {'value': [run]}}} + + AzureVCRBaseTest._response_substitutions(response) + + self.assertNotIn('createdBy', run) + self.assertNotIn('lastModifiedBy', run) + self.assertNotIn('userId', run) + self.assertEqual( + {'ComputeTargetType': 'AmlCompute'}, + run['properties'], + ) + + def test_recording_sanitizer_redacts_instrumentation_keys(self): + key = '12345678-1234-1234-1234-123456789abc' + body = ( + f'{{"InstrumentationKey": "{key}", ' + f'"sdkTelemetryAppInsightsKey": "{key}"}}' + ) + + sanitized = AzureVCRBaseTest._replace_instrumentation_key(body) + + self.assertNotIn(key, sanitized) + self.assertEqual(2, sanitized.count(DEFAULT_INSTRUMENTATION_KEY)) + + def test_recording_sanitizer_redacts_async_operation_signature(self): + url = ( + 'https://management.azure.com/computeOperationsStatus/operation-id' + '?api-version=2023-04-01&service=new&t=timestamp' + '&c=certificate&s=signature&h=hash' + ) + + sanitized = AzureVCRBaseTest._replace_async_operation_signature(url) + + self.assertIn('api-version=2023-04-01', sanitized) + self.assertIn('service=new', sanitized) + self.assertIn('t=timestamp', sanitized) + self.assertIn('c=redacted', sanitized) + self.assertIn('s=redacted', sanitized) + self.assertIn('h=redacted', sanitized) + self.assertNotIn('certificate', sanitized) + self.assertNotIn('signature', sanitized) + + def test_recording_sanitizer_redacts_operation_urls_without_json_body(self): + response = { + 'headers': { + 'azure-asyncoperation': [ + 'https://management.azure.com/providers/Microsoft.Test/' + 'operationResults/operation-id?api-version=2023-04-01' + '&c=certificate&s=signature&h=hash' + ], + 'location': [ + 'https://management.azure.com/providers/Microsoft.Test/' + 'operationStatuses/operation-id?api-version=2023-04-01' + '&c=certificate&s=signature&h=hash' + ], + }, + 'body': {'string': b''}, + } + + with patch.object(self, 'is_playback', return_value=False): + self._response_callback(response) + + for header in ('azure-asyncoperation', 'location'): + with self.subTest(header=header): + value = response['headers'][header][0] + self.assertIn('api-version=2023-04-01', value) + self.assertIn('c=redacted', value) + self.assertIn('s=redacted', value) + self.assertIn('h=redacted', value) + self.assertNotIn('certificate', value) + self.assertNotIn('signature', value) + + def test_recording_sanitizer_redacts_every_operation_url_shape(self): + for path in ( + 'operationsStatus', + 'operationStatuses', + 'operationResults', + ): + with self.subTest(path=path): + url = ( + 'https://management.azure.com/providers/Microsoft.Test/' + f'{path}/operation-id?api-version=2023-04-01' + '&c=certificate&s=signature&h=hash' + ) + + sanitized = AzureVCRBaseTest._replace_async_operation_signature( + url, + ) + + self.assertIn('c=redacted', sanitized) + self.assertIn('s=redacted', sanitized) + self.assertIn('h=redacted', sanitized) + self.assertNotIn('certificate', sanitized) + + def test_recording_sanitizer_removes_structured_experiment_identity(self): + experiment = { + 'experimentId': 'test-experiment', + 'name': 'cctest-current', + 'createdBy': { + 'userObjectId': 'private-object-id', + 'userTenantId': 'private-tenant-id', + 'userName': 'Test User', + 'upn': 'user@private.example', + }, + } + response = {'body': {'data': {'value': [experiment]}}} + + AzureVCRBaseTest._response_substitutions(response) + + self.assertNotIn('createdBy', experiment) + self.assertEqual('test-experiment', experiment['experimentId']) + + def test_recording_sanitizer_keeps_unrelated_requests_on_playback(self): + unrelated_request = Mock( + uri=( + 'https://management.azure.com/subscriptions/test/' + 'resourceGroups/private-resource-group/resources' + ), + body=None, + headers={}, + ) + + with patch.object(self, 'is_playback', return_value=True): + self.assertIs( + unrelated_request, + self._request_callback(unrelated_request), + ) diff --git a/tools/c7n_azure/tests_azure/test_azure_utils.py b/tools/c7n_azure/tests_azure/test_azure_utils.py index a340367ff80..683d0edb107 100644 --- a/tools/c7n_azure/tests_azure/test_azure_utils.py +++ b/tools/c7n_azure/tests_azure/test_azure_utils.py @@ -6,6 +6,7 @@ import pytest from azure.mgmt.managementgroups.models import DescendantInfo +from c7n_azure import utils from c7n_azure.tags import TagHelper from c7n_azure.utils import (AppInsightsHelper, ManagedGroupHelper, Math, PortsRangeHelper, ResourceIdParser, StringUtils, @@ -47,6 +48,42 @@ class UtilsTest(BaseTest): def setUp(self): super(UtilsTest, self).setUp() + def test_requests_session_uses_default_scope_and_get_retries(self): + azure_session = Mock(resource_endpoint='https://management.azure.com/') + azure_session.credentials.get_token.return_value.token = 'test-token' + + session = utils.requests_session(azure_session, max_retries=3) + + azure_session._initialize_session.assert_called_once_with() + azure_session.credentials.get_token.assert_called_once_with( + 'https://management.azure.com/.default', + ) + self.assertEqual('Bearer test-token', session.headers['Authorization']) + retry = session.get_adapter('https://').max_retries + self.assertEqual(3, retry.total) + self.assertEqual((429, 503), retry.status_forcelist) + self.assertEqual(('GET',), retry.allowed_methods) + self.assertTrue(retry.respect_retry_after_header) + self.assertFalse(retry.raise_on_status) + + def test_requests_session_accepts_scope_and_method_overrides(self): + azure_session = Mock(resource_endpoint='https://management.azure.com/') + azure_session.credentials.get_token.return_value.token = 'test-token' + + session = utils.requests_session( + azure_session, + token_scope='https://ml.azure.us/.default', + max_retries=2, + allowed_methods=('POST',), + ) + + azure_session.credentials.get_token.assert_called_once_with( + 'https://ml.azure.us/.default', + ) + retry = session.get_adapter('https://').max_retries + self.assertEqual(2, retry.total) + self.assertEqual(('POST',), retry.allowed_methods) + def test_get_subscription_id(self): self.assertEqual(ResourceIdParser.get_subscription_id(RESOURCE_ID), DEFAULT_SUBSCRIPTION_ID) diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_compute_cluster.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_compute_cluster.py new file mode 100644 index 00000000000..268d6f54b77 --- /dev/null +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_compute_cluster.py @@ -0,0 +1,1021 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import datetime +import time +from unittest.mock import ANY, call, Mock, patch + +import requests + +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.mgmt.machinelearningservices.models import ( + AmlCompute, + AmlComputeProperties, + ComputeInstance, + ComputeInstanceProperties, + ComputeResource, + NodeStateCounts, + ScaleSettings, +) + +from c7n.exceptions import PolicyValidationError +from c7n.utils import local_session +from c7n_azure.session import Session +from c7n_azure.utils import ResourceIdParser + +from ..azure_common import ( + BaseTest, + arm_template, + cassette_name, +) + + +class MachineLearningComputeClusterTest(BaseTest): + + recording_resource_group = 'test_machine-learning-compute-cluster' + parent_id = ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/' + 'resourceGroups/test-rg/providers/Microsoft.MachineLearningServices/' + 'workspaces/test-workspace' + ) + + def _load_inactive_filter(self, since='1d'): + policy = self.load_policy({ + 'name': 'inactive-machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [{ + 'type': 'inactive', + 'since': since, + }], + }) + return policy.resource_manager.filters[0] + + def _load_set_min_nodes_action(self): + policy = self.load_policy({ + 'name': 'set-machine-learning-compute-cluster-minimum-nodes', + 'resource': 'azure.machine-learning-compute-cluster', + 'actions': [{ + 'type': 'set-min-nodes', + 'value': 0, + }], + }) + return policy.resource_manager.actions[0] + + def _scale_cluster(self): + return { + 'id': f'{self.parent_id}/computes/test-cluster', + 'name': 'test-cluster', + 'c7n:parent-id': self.parent_id, + 'properties': { + 'properties': { + 'scaleSettings': { + 'minNodeCount': 1, + 'maxNodeCount': 4, + 'nodeIdleTimeBeforeScaleDown': 'PT5M', + }, + }, + }, + } + + def _cluster( + self, + name, + running_node_count=0, + parent_id=None, + discovery_url='https://westus.api.azureml.ms/discovery', + ): + return { + 'name': name, + 'c7n:parent-id': parent_id or self.parent_id, + 'c7n:WorkspaceDiscoveryUrl': discovery_url, + 'properties': { + 'properties': { + 'nodeStateCounts': { + 'runningNodeCount': running_node_count, + }, + }, + }, + } + + def test_machine_learning_compute_cluster_schema_validate(self): + p = self.load_policy({ + 'name': 'machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + }, validate=True) + self.assertTrue(p) + + @arm_template('machine-learning-compute-cluster.json') + @cassette_name('machine-learning-compute-cluster-query') + def test_machine_learning_compute_cluster_policy_query(self): + policy = self.load_policy({ + 'name': 'find-machine-learning-compute-cluster', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-compute-cluster', + }, + { + 'type': 'value', + 'key': 'name', + 'value': 'cctest-ml-cluster', + }, + { + 'type': 'value', + 'key': 'properties.computeType', + 'value': 'AmlCompute', + }, + ], + }) + + resources = policy.run() + + self.assertEqual(1, len(resources)) + self.assertEqual('cctest-ml-cluster', resources[0]['name']) + self.assertEqual( + 'test_machine-learning-compute-cluster', + resources[0]['resourceGroup'], + ) + + def test_machine_learning_compute_cluster_child_query(self): + parent_id = ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/' + 'resourceGroups/test-rg/providers/Microsoft.MachineLearningServices/' + 'workspaces/test-workspace' + ) + cluster_properties = AmlComputeProperties( + vm_size='Standard_DS2_v2', + scale_settings=ScaleSettings( + max_node_count=4, + min_node_count=1, + node_idle_time_before_scale_down=datetime.timedelta(minutes=5), + ), + ) + cluster_properties.current_node_count = 2 + cluster_properties.node_state_counts = NodeStateCounts() + cluster_properties.node_state_counts.running_node_count = 1 + cluster = ComputeResource( + properties=AmlCompute(properties=cluster_properties), + location='westus', + ) + cluster.id = f'{parent_id}/computes/test-cluster' + cluster.name = 'test-cluster' + cluster.type = 'Microsoft.MachineLearningServices/workspaces/computes' + + instance = ComputeResource( + properties=ComputeInstance( + properties=ComputeInstanceProperties(vm_size='Standard_DS2_v2'), + ), + location='westus', + ) + instance.id = f'{parent_id}/computes/test-instance' + instance.name = 'test-instance' + instance.type = 'Microsoft.MachineLearningServices/workspaces/computes' + + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [{ + 'id': parent_id, + 'name': 'test-workspace', + 'resourceGroup': 'test-rg', + 'properties': { + 'discoveryUrl': 'https://westus.api.azureml.ms/discovery', + }, + }] + + client = Mock() + client.compute.list.return_value = [cluster, instance] + + p = self.load_policy({ + 'name': 'machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + }) + manager = p.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + manager.get_client = Mock(return_value=client) + + resources = manager.resources() + + client.compute.list.assert_called_once_with( + resource_group_name='test-rg', + workspace_name='test-workspace', + ) + self.assertEqual(['test-cluster'], [r['name'] for r in resources]) + self.assertEqual(parent_id, resources[0]['c7n:parent-id']) + self.assertEqual( + 'https://westus.api.azureml.ms/discovery', + resources[0]['c7n:WorkspaceDiscoveryUrl'], + ) + properties = resources[0]['properties']['properties'] + self.assertEqual(2, properties['currentNodeCount']) + self.assertEqual(1, properties['nodeStateCounts']['runningNodeCount']) + self.assertEqual(1, properties['scaleSettings']['minNodeCount']) + self.assertEqual(4, properties['scaleSettings']['maxNodeCount']) + + def test_child_query_skips_stale_workspace(self): + stale_parent_id = self.parent_id.replace( + 'test-workspace', + 'stale-workspace', + ) + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [ + { + 'id': stale_parent_id, + 'name': 'stale-workspace', + 'resourceGroup': 'missing-rg', + 'properties': { + 'discoveryUrl': 'https://westus.api.azureml.ms/discovery', + }, + }, + { + 'id': self.parent_id, + 'name': 'test-workspace', + 'resourceGroup': 'test-rg', + 'properties': { + 'discoveryUrl': 'https://westus.api.azureml.ms/discovery', + }, + }, + ] + cluster = ComputeResource( + properties=AmlCompute( + properties=AmlComputeProperties(vm_size='Standard_DS2_v2'), + ), + location='westus', + ) + cluster.id = f'{self.parent_id}/computes/test-cluster' + cluster.name = 'test-cluster' + cluster.type = ( + 'Microsoft.MachineLearningServices/workspaces/computes' + ) + instance = ComputeResource( + properties=ComputeInstance( + properties=ComputeInstanceProperties(vm_size='Standard_DS2_v2'), + ), + location='westus', + ) + instance.id = f'{self.parent_id}/computes/test-instance' + instance.name = 'test-instance' + client = Mock() + client.compute.list.side_effect = [ + ResourceNotFoundError('stale workspace'), + [cluster, instance], + ] + policy = self.load_policy({ + 'name': 'machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + }) + manager = policy.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + manager.get_client = Mock(return_value=client) + + with self.assertLogs('custodian.azure.query', level='WARNING') as logs: + resources = manager.resources() + + self.assertEqual(['test-cluster'], [r['name'] for r in resources]) + self.assertEqual(self.parent_id, resources[0]['c7n:parent-id']) + self.assertEqual( + 'https://westus.api.azureml.ms/discovery', + resources[0]['c7n:WorkspaceDiscoveryUrl'], + ) + self.assertEqual( + [ + call( + resource_group_name='missing-rg', + workspace_name='stale-workspace', + ), + call( + resource_group_name='test-rg', + workspace_name='test-workspace', + ), + ], + client.compute.list.call_args_list, + ) + self.assertIn(stale_parent_id, logs.output[0]) + + def test_child_query_defaults_missing_discovery_url(self): + policy = self.load_policy({ + 'name': 'machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + }) + cluster = { + 'name': 'test-cluster', + 'properties': {'computeType': 'AmlCompute'}, + } + parent = { + 'name': 'test-workspace', + 'resourceGroup': 'test-rg', + 'properties': {}, + } + + with patch( + 'c7n_azure.resources.arm.ChildArmResourceManager.' + 'enumerate_resources', + return_value=[cluster], + ): + resources = policy.resource_manager.enumerate_resources( + parent, + policy.resource_manager.resource_type, + ) + + self.assertIsNone(resources[0]['c7n:WorkspaceDiscoveryUrl']) + + def _run_inactive_policy(self, since): + policy = self.load_policy({ + 'name': 'find-inactive-machine-learning-compute-cluster', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value': 'test_machine-learning-compute-cluster', + }, + { + 'type': 'value', + 'key': 'name', + 'value': 'cctest-ml-cluster', + }, + { + 'type': 'inactive', + 'since': since, + }, + ], + }) + return policy.run() + + @arm_template('machine-learning-compute-cluster.json') + @cassette_name('machine-learning-compute-cluster-inactive-current') + def test_machine_learning_compute_cluster_inactive_current_policy(self): + resources = self._run_inactive_policy('1m') + + self.assertEqual([], resources) + + @arm_template('machine-learning-compute-cluster.json') + @cassette_name('machine-learning-compute-cluster-inactive-recent') + def test_machine_learning_compute_cluster_inactive_recent_policy(self): + resources = self._run_inactive_policy('1d') + + self.assertEqual([], resources) + + def test_set_min_nodes_schema_validate(self): + policy = self.load_policy({ + 'name': 'set-machine-learning-compute-cluster-minimum-nodes', + 'resource': 'azure.machine-learning-compute-cluster', + 'actions': [{ + 'type': 'set-min-nodes', + 'value': 0, + }], + }, validate=True) + + self.assertTrue(policy) + + def test_set_min_nodes_schema_rejects_invalid_values(self): + for action in ( + {'type': 'set-min-nodes'}, + {'type': 'set-min-nodes', 'value': -1}, + {'type': 'set-min-nodes', 'value': 1.5}, + {'type': 'set-min-nodes', 'value': 0, 'state': 'idle'}, + ): + with self.subTest(action=action): + with self.assertRaises(PolicyValidationError): + self.load_policy({ + 'name': 'set-machine-learning-compute-cluster-minimum-nodes', + 'resource': 'azure.machine-learning-compute-cluster', + 'actions': [action], + }, validate=True) + + @arm_template('machine-learning-compute-cluster.json') + @cassette_name('machine-learning-compute-cluster-set-min-nodes') + def test_set_min_nodes_policy(self): + resource_group = 'test_machine-learning-compute-cluster' + cluster_name = 'cctest-ml-cluster' + query_policy = self.load_policy({ + 'name': 'get-machine-learning-compute-cluster-for-update', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value': resource_group, + }, + { + 'type': 'value', + 'key': 'name', + 'value': cluster_name, + }, + ], + }) + clusters = query_policy.run() + self.assertEqual(1, len(clusters)) + workspace_name = ResourceIdParser.get_resource_name( + clusters[0]['c7n:parent-id'] + ) + client = local_session(Session).client( + 'azure.mgmt.machinelearningservices.' + 'MachineLearningServicesMgmtClient' + ) + setup_policy = self.load_policy({ + 'name': 'prepare-machine-learning-compute-cluster-minimum-nodes', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value': resource_group, + }, + { + 'type': 'value', + 'key': 'name', + 'value': cluster_name, + }, + ], + 'actions': [{ + 'type': 'set-min-nodes', + 'value': 1, + }], + }) + self.assertEqual(1, len(setup_policy.run())) + for _ in range(60): + cluster = client.compute.get( + resource_group, + workspace_name, + cluster_name, + ) + scale_settings = cluster.properties.properties.scale_settings + if ( + cluster.properties.provisioning_state == 'Succeeded' + and scale_settings.min_node_count == 1 + ): + break + if not self.is_playback(): + time.sleep(5) + else: + self.fail('compute cluster did not reach minimum one') + + action_policy = self.load_policy({ + 'name': 'set-machine-learning-compute-cluster-minimum-nodes', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value': resource_group, + }, + { + 'type': 'value', + 'key': 'name', + 'value': cluster_name, + }, + { + 'type': 'value', + 'key': 'properties.properties.scaleSettings.minNodeCount', + 'value': 1, + }, + ], + 'actions': [{ + 'type': 'set-min-nodes', + 'value': 0, + }], + }) + resources = action_policy.run() + self.assertEqual(1, len(resources)) + + cluster = client.compute.get( + resource_group, + workspace_name, + cluster_name, + ) + scale_settings = cluster.properties.properties.scale_settings + self.assertEqual(0, scale_settings.min_node_count) + self.assertEqual(1, scale_settings.max_node_count) + self.assertEqual( + datetime.timedelta(minutes=2), + scale_settings.node_idle_time_before_scale_down, + ) + + def test_set_min_nodes_accepts_accepted_response(self): + action = self._load_set_min_nodes_action() + client = Mock() + client.compute.begin_update.side_effect = HttpResponseError( + response=Mock(status_code=202, reason='Accepted'), + ) + action.manager.get_client = Mock(return_value=client) + + action._prepare_processing() + action._process_resource(self._scale_cluster()) + + client.compute.begin_update.assert_called_once() + + def test_set_min_nodes_reraises_non_accepted_response(self): + action = self._load_set_min_nodes_action() + error = HttpResponseError( + response=Mock(status_code=409, reason='Conflict'), + ) + client = Mock() + client.compute.begin_update.side_effect = error + action.manager.get_client = Mock(return_value=client) + + action._prepare_processing() + with self.assertRaises(HttpResponseError) as caught: + action._process_resource(self._scale_cluster()) + + self.assertIs(error, caught.exception) + + def test_set_min_nodes_exact_update_request(self): + action = self._load_set_min_nodes_action() + client = Mock() + action.manager.get_client = Mock(return_value=client) + + action._prepare_processing() + action._process_resource(self._scale_cluster()) + + self.assertEqual(1, client.compute.begin_update.call_count) + kwargs = client.compute.begin_update.call_args.kwargs + self.assertEqual('test-rg', kwargs['resource_group_name']) + self.assertEqual('test-workspace', kwargs['workspace_name']) + self.assertEqual('test-cluster', kwargs['compute_name']) + scale = kwargs['parameters'].properties.scale_settings + self.assertEqual(0, scale.min_node_count) + self.assertEqual(4, scale.max_node_count) + self.assertEqual( + datetime.timedelta(minutes=5), + scale.node_idle_time_before_scale_down, + ) + self.assertEqual( + { + 'properties': { + 'properties': { + 'scaleSettings': { + 'minNodeCount': 0, + 'maxNodeCount': 4, + 'nodeIdleTimeBeforeScaleDown': 'PT5M', + }, + }, + }, + }, + kwargs['parameters'].serialize(), + ) + + def test_set_min_nodes_keeps_unset_idle_time_unset(self): + action = self._load_set_min_nodes_action() + client = Mock() + action.manager.get_client = Mock(return_value=client) + resource = self._scale_cluster() + del resource['properties']['properties']['scaleSettings'][ + 'nodeIdleTimeBeforeScaleDown' + ] + + action._prepare_processing() + action._process_resource(resource) + + kwargs = client.compute.begin_update.call_args.kwargs + self.assertEqual( + { + 'properties': { + 'properties': { + 'scaleSettings': { + 'minNodeCount': 0, + 'maxNodeCount': 4, + }, + }, + }, + }, + kwargs['parameters'].serialize(), + ) + + def test_inactive_schema_validate(self): + policy = self.load_policy({ + 'name': 'inactive-machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [{ + 'type': 'inactive', + 'since': '1d', + }], + }, validate=True) + + self.assertTrue(policy) + + def test_inactive_schema_rejects_invalid_values(self): + for inactive_filter in ( + {'type': 'inactive'}, + {'type': 'inactive', 'since': '0d'}, + {'type': 'inactive', 'since': '-1d'}, + {'type': 'inactive', 'since': '1y'}, + {'type': 'inactive', 'since': '1d', 'state': 'idle'}, + ): + with self.subTest(inactive_filter=inactive_filter): + with self.assertRaises(PolicyValidationError): + self.load_policy({ + 'name': 'inactive-machine-learning-compute-clusters', + 'resource': 'azure.machine-learning-compute-cluster', + 'filters': [inactive_filter], + }, validate=True) + + def test_inactive_short_circuit_running_clusters(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(return_value=set()) + clusters = [ + self._cluster('running-cluster', running_node_count=1), + self._cluster('idle-cluster'), + ] + + resources = inactive_filter.process(clusters) + + self.assertEqual(['idle-cluster'], [r['name'] for r in resources]) + inactive_filter._get_active_targets.assert_called_once() + + def test_inactive_short_circuit_skips_history_for_running_clusters(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(return_value=set()) + + resources = inactive_filter.process([ + self._cluster('running-cluster', running_node_count=1), + ]) + + self.assertEqual([], resources) + inactive_filter._get_active_targets.assert_not_called() + + def test_inactive_cluster_without_node_state_counts(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(return_value=set()) + cluster = self._cluster('provisioning-cluster') + cluster['properties'] = {} + + resources = inactive_filter.process([cluster]) + + self.assertEqual(['provisioning-cluster'], [r['name'] for r in resources]) + + def test_inactive_cluster_with_null_node_state_counts(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(return_value=set()) + null_counts = self._cluster('failed-cluster') + null_counts['properties']['properties']['nodeStateCounts'] = None + null_running_count = self._cluster('creating-cluster') + null_running_count['properties']['properties'][ + 'nodeStateCounts' + ]['runningNodeCount'] = None + + resources = inactive_filter.process([ + null_counts, + null_running_count, + ]) + + self.assertEqual( + ['failed-cluster', 'creating-cluster'], + [r['name'] for r in resources], + ) + + @patch( + 'c7n_azure.resources.machine_learning_compute_cluster.utils.requests_session', + ) + def test_inactive_history_pagination(self, requests_session): + inactive_filter = self._load_inactive_filter() + azure_session = Mock() + azure_session.cloud_endpoints.name = 'AzureCloud' + inactive_filter.manager.get_session = Mock(return_value=azure_session) + first_response = Mock() + first_response.json.return_value = { + 'value': [{'experimentId': 'first'}], + 'continuationToken': 'next-page', + } + second_response = Mock() + second_response.json.return_value = { + 'value': [{'experimentId': 'second'}], + 'continuationToken': None, + } + http = requests_session.return_value + http.post.side_effect = [first_response, second_response] + url = 'https://westus.api.azureml.ms/history/v1.0/experiments:query' + + values = inactive_filter._query_history( + url, + {'viewType': 'ActiveOnly'}, + ) + + self.assertEqual( + [{'experimentId': 'first'}, {'experimentId': 'second'}], + values, + ) + requests_session.assert_called_once_with( + azure_session, + token_scope='https://ml.azure.com/.default', + max_retries=3, + allowed_methods=('POST',), + ) + self.assertEqual(2, http.post.call_count) + self.assertEqual( + {'api-version': '2023-10-01'}, + http.post.call_args_list[0].kwargs['params'], + ) + self.assertEqual( + {'viewType': 'ActiveOnly'}, + http.post.call_args_list[0].kwargs['json'], + ) + self.assertEqual( + { + 'viewType': 'ActiveOnly', + 'continuationToken': 'next-page', + }, + http.post.call_args_list[1].kwargs['json'], + ) + for post_call in http.post.call_args_list: + self.assertEqual(30, post_call.kwargs['timeout']) + self.assertEqual( + {'Content-Type': 'application/json'}, + post_call.kwargs['headers'], + ) + first_response.raise_for_status.assert_called_once_with() + second_response.raise_for_status.assert_called_once_with() + + @patch( + 'c7n_azure.resources.machine_learning_compute_cluster.utils.requests_session', + ) + def test_inactive_history_pagination_rejects_missing_value( + self, + requests_session, + ): + inactive_filter = self._load_inactive_filter() + azure_session = Mock() + azure_session.cloud_endpoints.name = 'AzureCloud' + inactive_filter.manager.get_session = Mock(return_value=azure_session) + response = Mock() + response.json.return_value = {} + requests_session.return_value.post.return_value = response + + with self.assertRaises(TypeError): + inactive_filter._query_history( + 'https://westus.api.azureml.ms/history/v1.0/experiments:query', + {'viewType': 'ActiveOnly'}, + ) + + response.raise_for_status.assert_called_once_with() + + @patch( + 'c7n_azure.resources.machine_learning_compute_cluster.utils.requests_session', + ) + def test_inactive_history_audience_follows_cloud(self, requests_session): + inactive_filter = self._load_inactive_filter() + azure_session = Mock() + inactive_filter.manager.get_session = Mock(return_value=azure_session) + requests_session.return_value.post.return_value.json.return_value = { + 'value': [], + } + + for cloud, audience in ( + ('AzureCloud', 'https://ml.azure.com/.default'), + ('AzureChinaCloud', 'https://ml.azure.cn/.default'), + ('AzureUSGovernment', 'https://ml.azure.us/.default'), + ): + with self.subTest(cloud=cloud): + azure_session.cloud_endpoints.name = cloud + requests_session.reset_mock() + + inactive_filter._query_history( + 'https://westus.api.azureml.ms/history/v1.0/runs:query', + {}, + ) + + requests_session.assert_called_once_with( + azure_session, + token_scope=audience, + max_retries=3, + allowed_methods=('POST',), + ) + + def test_inactive_experiments_include_archived(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._query_history = Mock(return_value=[ + {'experimentId': 'active'}, + {'experimentId': 'archived'}, + {'experimentId': 'archived'}, + {'experimentId': ''}, + ]) + + experiments = inactive_filter._get_experiments(self._cluster('cluster')) + + self.assertEqual(['active', 'archived'], experiments) + url = ( + f'https://westus.api.azureml.ms/history/v1.0{self.parent_id}/' + 'experiments:query' + ) + self.assertEqual( + [call(url, {'viewType': 'All'})], + inactive_filter._query_history.call_args_list, + ) + + def test_inactive_active_targets_include_standalone_and_pipeline_runs(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_experiments = Mock( + return_value=['experiment-one', 'experiment-two'], + ) + inactive_filter._query_history = Mock(side_effect=[ + [ + {'runId': 'standalone', 'target': 'Cluster-One'}, + {'runId': 'pipeline-parent', 'target': None}, + ], + [{'runId': 'pipeline-child', 'target': 'CLUSTER-TWO'}], + [{'runId': 'second-standalone', 'target': 'Cluster-Three'}], + [{'runId': 'targetless', 'target': ''}], + ]) + cutoff = datetime.datetime( + 2024, + 1, + 2, + 3, + 4, + 5, + tzinfo=datetime.timezone.utc, + ) + + targets = inactive_filter._get_active_targets( + self._cluster('cluster'), + cutoff, + ) + + self.assertEqual( + {'cluster-one', 'cluster-two', 'cluster-three'}, + targets, + ) + statuses = ( + 'NotStarted', + 'Starting', + 'Provisioning', + 'Preparing', + 'Queued', + 'Running', + 'Finalizing', + 'CancelRequested', + 'NotResponding', + 'Unapproved', + 'Pausing', + 'Paused', + ) + status_filter = ' or '.join( + f"status eq '{status}'" for status in statuses + ) + base_url = ( + f'https://westus.api.azureml.ms/history/v1.0{self.parent_id}/' + 'experimentids' + ) + self.assertEqual( + [ + call( + f'{base_url}/experiment-one/runs:query', + {'filter': status_filter}, + ), + call( + f'{base_url}/experiment-one/runs:query', + {'filter': 'endTimeUtc ge 2024-01-02T03:04:05Z'}, + ), + call( + f'{base_url}/experiment-two/runs:query', + {'filter': status_filter}, + ), + call( + f'{base_url}/experiment-two/runs:query', + {'filter': 'endTimeUtc ge 2024-01-02T03:04:05Z'}, + ), + ], + inactive_filter._query_history.call_args_list, + ) + + def test_inactive_skips_workspace_without_discovery_url(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(return_value=set()) + missing_discovery = self._cluster('missing-discovery') + missing_discovery.pop('c7n:WorkspaceDiscoveryUrl') + east_parent_id = self.parent_id.replace( + 'test-workspace', + 'east-workspace', + ) + idle_east = self._cluster( + 'idle-east', + parent_id=east_parent_id, + discovery_url='https://eastus.api.azureml.ms/discovery', + ) + + with self.assertLogs(inactive_filter.log, level='WARNING') as logs: + resources = inactive_filter.process([ + missing_discovery, + idle_east, + ]) + + self.assertEqual(['idle-east'], [r['name'] for r in resources]) + self.assertIn(self.parent_id, logs.output[0]) + inactive_filter._get_active_targets.assert_called_once_with( + idle_east, + ANY, + ) + + def test_inactive_skips_workspace_on_request_failure(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(side_effect=[ + requests.RequestException('Run History unavailable'), + {'active-east'}, + ]) + east_parent_id = self.parent_id.replace( + 'test-workspace', + 'east-workspace', + ) + clusters = [ + self._cluster('idle-west'), + self._cluster( + 'active-east', + parent_id=east_parent_id, + discovery_url='https://eastus.api.azureml.ms/discovery', + ), + self._cluster( + 'idle-east', + parent_id=east_parent_id, + discovery_url='https://eastus.api.azureml.ms/discovery', + ), + ] + + with self.assertLogs(inactive_filter.log, level='WARNING') as logs: + resources = inactive_filter.process(clusters) + + self.assertEqual(['idle-east'], [r['name'] for r in resources]) + self.assertIn(self.parent_id, logs.output[0]) + + def test_inactive_workspace_cache(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_active_targets = Mock(side_effect=[ + {'active-west'}, + {'active-east'}, + ]) + east_parent_id = self.parent_id.replace( + 'test-workspace', + 'east-workspace', + ) + clusters = [ + self._cluster('active-west'), + self._cluster('idle-west'), + self._cluster( + 'active-east', + parent_id=east_parent_id, + discovery_url='https://eastus.api.azureml.ms/discovery', + ), + ] + + resources = inactive_filter.process(clusters) + + self.assertEqual(['idle-west'], [r['name'] for r in resources]) + self.assertEqual(2, inactive_filter._get_active_targets.call_count) + history_calls = inactive_filter._get_active_targets.call_args_list + self.assertEqual('active-west', history_calls[0].args[0]['name']) + self.assertEqual('active-east', history_calls[1].args[0]['name']) + + def test_inactive_active_targets_use_separate_workspace_endpoints(self): + inactive_filter = self._load_inactive_filter() + inactive_filter._get_experiments = Mock(side_effect=[ + ['west-experiment'], + ['east-experiment'], + ]) + inactive_filter._query_history = Mock(side_effect=[ + [{'target': 'west-cluster'}], + [], + [{'target': 'east-cluster'}], + [], + ]) + cutoff = datetime.datetime( + 2024, + 1, + 2, + 3, + 4, + 5, + tzinfo=datetime.timezone.utc, + ) + east_parent_id = self.parent_id.replace( + 'test-workspace', + 'east-workspace', + ) + + west_targets = inactive_filter._get_active_targets( + self._cluster('west-cluster'), + cutoff, + ) + east_targets = inactive_filter._get_active_targets( + self._cluster( + 'east-cluster', + parent_id=east_parent_id, + discovery_url='https://eastus.api.azureml.ms/discovery', + ), + cutoff, + ) + + self.assertEqual({'west-cluster'}, west_targets) + self.assertEqual({'east-cluster'}, east_targets) + urls = [ + history_call.args[0] + for history_call in inactive_filter._query_history.call_args_list + ] + self.assertTrue(all( + url.startswith('https://westus.api.azureml.ms/') + for url in urls[:2] + )) + self.assertTrue(all( + url.startswith('https://eastus.api.azureml.ms/') + for url in urls[2:] + )) From 5d868efb95d4a7d92b3834ff3d662fc5acd18c9a Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Tue, 13 Oct 2026 13:10:21 -0400 Subject: [PATCH 38/65] azure - add machine-learning-environment-version resource (#11159) --- .../machine_learning_environment_version.py | 84 ++ .../c7n_azure/resources/resource_map.py | 1 + ...machine-learning-environment-versions.json | 787 ++++++++++++++++++ .../machine-learning-environment-version.json | 130 +++ ...st_machine_learning_environment_version.py | 171 ++++ 5 files changed, 1173 insertions(+) create mode 100644 tools/c7n_azure/c7n_azure/resources/machine_learning_environment_version.py create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningEnvironmentVersionTest.machine-learning-environment-versions.json create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-environment-version.json create mode 100644 tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_environment_version.py diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_environment_version.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_environment_version.py new file mode 100644 index 00000000000..e3f9c9b4ac3 --- /dev/null +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_environment_version.py @@ -0,0 +1,84 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from azure.core.exceptions import ResourceNotFoundError +from c7n_azure.provider import resources +from c7n_azure.resources.arm import ChildArmResourceManager +from c7n_azure.utils import ResourceIdParser + + +@resources.register('machine-learning-environment-version') +class MachineLearningEnvironmentVersion(ChildArmResourceManager): + """Machine Learning Environment Version Resource + + Enumerates every environment version in each Machine Learning + workspace, including archived versions and versions of archived + environments. + + ``properties.isArchived`` is only true for a version archived on its + own. Archiving a whole environment does not set it on its versions. + + :example: + + Find user-created environment versions that are archived and older + than 30 days. + + .. code-block:: yaml + + policies: + - name: azure-ml-stale-archived-environment-versions + resource: azure.machine-learning-environment-version + filters: + - type: value + key: properties.environmentType + value: UserCreated + - type: value + key: properties.isArchived + value: true + - type: value + key: systemData.createdAt + op: gt + value_type: age + value: 30 + """ + + class resource_type(ChildArmResourceManager.resource_type): + doc_groups = ['ML'] + + service = 'azure.mgmt.machinelearningservices' + client = 'MachineLearningServicesMgmtClient' + enum_spec = ('environment_versions', 'list', None) + parent_manager_name = 'machine-learning-workspace' + resource_type = 'Microsoft.MachineLearningServices/workspaces/environments/versions' + default_report_fields = ( + 'name', + 'resourceGroup', + '"c7n:parent-id"' + ) + + def enumerate_resources(self, parent_resource, type_info, vault_url=None, **params): + client = self.get_client() + resource_group = ResourceIdParser.get_resource_group(parent_resource['id']) + workspace_name = parent_resource['name'] + + versions = [] + try: + for container in client.environment_containers.list( + resource_group_name=resource_group, + workspace_name=workspace_name, + list_view_type="All"): + try: + for version in client.environment_versions.list( + resource_group_name=resource_group, + workspace_name=workspace_name, + name=container.name, + list_view_type="All"): + versions.append(version.serialize(True)) + except ResourceNotFoundError: + # Curated environments are listed, but their versions live in a + # Microsoft registry. + continue + except ResourceNotFoundError: + # A workspace stuck deleting is still listed after its resource group is gone. + return [] + return versions diff --git a/tools/c7n_azure/c7n_azure/resources/resource_map.py b/tools/c7n_azure/c7n_azure/resources/resource_map.py index 7dd4f3fddbf..dcb1e854d35 100644 --- a/tools/c7n_azure/c7n_azure/resources/resource_map.py +++ b/tools/c7n_azure/c7n_azure/resources/resource_map.py @@ -70,6 +70,7 @@ "azure.machine-learning-workspace": "c7n_azure.resources.machine_learning.MachineLearningWorkspace", # noqa "azure.machine-learning-compute-cluster": "c7n_azure.resources.machine_learning_compute_cluster.MachineLearningComputeCluster", # noqa "azure.machine-learning-data-container": "c7n_azure.resources.machine_learning.MachineLearningDataContainer", # noqa + "azure.machine-learning-environment-version": "c7n_azure.resources.machine_learning_environment_version.MachineLearningEnvironmentVersion", # noqa "azure.machine-learning-job": "c7n_azure.resources.machine_learning_job.MachineLearningJob", "azure.machine-learning-model-version": "c7n_azure.resources.machine_learning_model_version.MachineLearningModelVersion", # noqa "azure.machine-learning-online-endpoint": "c7n_azure.resources.machine_learning_online_endpoint.MachineLearningOnlineEndpoint", # noqa diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningEnvironmentVersionTest.machine-learning-environment-versions.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningEnvironmentVersionTest.machine-learning-environment-versions.json new file mode 100644 index 00000000000..abaff313afa --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningEnvironmentVersionTest.machine-learning-environment-versions.json @@ -0,0 +1,787 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-type": [ + "application/json; charset=utf-8" + ], + "x-msedge-ref": [ + "Ref A: 3AF01E96D1D243A480D2D22E4C61C57D Ref B: CHI261140514062 Ref C: 2026-09-29T20:56:45Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "content-length": [ + "12912" + ], + "date": [ + "Tue, 29 Sep 2026 20:56:45 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863", + "name": "cctestmlworkspace299470296863", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.Storage/storageAccounts/cctestml299470296863", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.Keyvault/vaults/cctestmlkv299470296863", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.insights/components/cctestmlai299470296863", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-09-29T20:55:26.7028901Z", + "notebookInfo": { + "resourceId": "3f25e776b89a4d8baabb3c3bb87d8120", + "fqdn": "ml-cctestmlworkspac-eastus-48c3feb7-e1c6-4b0c-8f0b-b92baacdcf04.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "48c3feb7-e1c6-4b0c-8f0b-b92baacdcf04", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863", + "sdkTelemetryAppInsightsKey": "71fa86c2-fe23-49db-bb2e-8537edfb6ead", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "ed42de26-bd36-4cb2-9039-1e2ec3a18aa7", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:26.7028901Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:26.7028901Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: CCF286338D85447DAE7B63026A1233A1 Ref B: CHI261140505042 Ref C: 2026-09-29T20:56:52Z" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.349" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "2744" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ], + "date": [ + "Tue, 29 Sep 2026 20:56:52 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-env", + "name": "cctest-env", + "type": "Microsoft.MachineLearningServices/workspaces/environments", + "properties": { + "description": "", + "tags": {}, + "properties": {}, + "isArchived": false, + "latestVersion": "2", + "nextVersion": "3", + "provisioningState": "Succeeded" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:44.8490067+00:00", + "createdBy": "Liam Callahan", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:44.8490067+00:00", + "lastModifiedBy": "Liam Callahan", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-archived-env", + "name": "cctest-archived-env", + "type": "Microsoft.MachineLearningServices/workspaces/environments", + "properties": { + "description": "", + "tags": {}, + "properties": {}, + "isArchived": false, + "latestVersion": "1", + "nextVersion": "2", + "provisioningState": "Succeeded" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:44.835229+00:00", + "createdBy": "Liam Callahan", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:44.835229+00:00", + "lastModifiedBy": "Liam Callahan", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu", + "name": "AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu", + "type": "Microsoft.MachineLearningServices/workspaces/environments", + "properties": { + "description": "", + "tags": {}, + "properties": {}, + "isArchived": false, + "latestVersion": "10", + "nextVersion": "11", + "provisioningState": "Succeeded" + }, + "systemData": { + "createdAt": "2023-01-24T02:27:55.6397629+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-01-24T02:27:55.6397629+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-env/versions?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 7CBBD0A5AB26477B9AA9CFD5A0169EE4 Ref B: CHI261140514031 Ref C: 2026-09-29T20:56:53Z" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.255" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "2315" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ], + "date": [ + "Tue, 29 Sep 2026 20:56:53 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-env/versions/2", + "name": "2", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": null, + "tags": {}, + "properties": { + "azureml.labels": "latest" + }, + "isArchived": true, + "isAnonymous": false, + "environmentType": "UserCreated", + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "Disabled", + "provisioningState": "Succeeded", + "stage": "Archived" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:46.7756266+00:00", + "createdBy": "Liam Callahan", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:46.7756266+00:00", + "lastModifiedBy": "Liam Callahan", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-env/versions/1", + "name": "1", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": null, + "tags": {}, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "UserCreated", + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "Disabled", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:44.8833007+00:00", + "createdBy": "Liam Callahan", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:44.8833007+00:00", + "lastModifiedBy": "Liam Callahan", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-archived-env/versions?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: 45003D08C4D745428A4AD8263DB18674 Ref B: CH1AA2020603035 Ref C: 2026-09-29T20:56:53Z" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.210" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "1181" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ], + "date": [ + "Tue, 29 Sep 2026 20:56:54 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/cctest-archived-env/versions/1", + "name": "1", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": null, + "tags": {}, + "properties": { + "azureml.labels": "latest" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "UserCreated", + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "Disabled", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2026-09-29T20:55:44.8710568+00:00", + "createdBy": "Liam Callahan", + "createdByType": "User", + "lastModifiedAt": "2026-09-29T20:55:44.8710568+00:00", + "lastModifiedBy": "Liam Callahan", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-msedge-ref": [ + "Ref A: B647204286C84A808AC6CFD6D3F76E85 Ref B: CH1AA2020610021 Ref C: 2026-09-29T20:56:54Z" + ], + "cache-control": [ + "no-cache" + ], + "x-request-time": [ + "0.877" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "13372" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-02" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-response-type": [ + "standard" + ], + "date": [ + "Tue, 29 Sep 2026 20:56:54 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/10", + "name": "10", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "latest" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-09-13T01:54:26.1991188+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-09-13T01:54:26.1991188+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/9", + "name": "9", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-08-24T06:17:57.9196535+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-08-24T06:17:57.9196535+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/8", + "name": "8", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-08-18T19:34:13.026051+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-08-18T19:34:13.026051+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/7", + "name": "7", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-07-26T04:07:35.2445597+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-07-26T04:07:35.2445597+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/6", + "name": "6", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-06-22T17:45:42.4949246+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-06-22T17:45:42.4949246+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/5", + "name": "5", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-06-02T17:24:07.3380232+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-06-02T17:24:07.3380232+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/4", + "name": "4", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-04-21T03:58:31.3220762+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-04-21T03:58:31.3220762+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/3", + "name": "3", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-02-26T04:25:41.3956691+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-02-26T04:25:41.3956691+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-environment-version/providers/Microsoft.MachineLearningServices/workspaces/cctestmlworkspace299470296863/environments/AzureML-ACPT-pytorch-1.13-py38-cuda11.7-gpu/versions/1", + "name": "1", + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "properties": { + "description": "Recommended environment for Deep Learning in public preview with PyTorch on Azure containing the Azure ML SDK with the latest compatible versions of Ubuntu, Python, PyTorch, CUDA\\RocM, combined with optimizers like ORT Training,+DeepSpeed+MSCCL+ORT MoE and more.", + "tags": { + "PyTorch": "1.13.0", + "GPU": "Cuda11", + "OS": "Ubuntu20.04", + "Training": "" + }, + "properties": { + "azureml.labels": "" + }, + "isArchived": false, + "isAnonymous": false, + "environmentType": "Curated", + "condaFile": "", + "osType": "Linux", + "autoRebuild": "OnBaseImageUpdate", + "provisioningState": "Succeeded", + "stage": "Development" + }, + "systemData": { + "createdAt": "2023-01-24T02:27:55.6107431+00:00", + "createdBy": "Microsoft", + "createdByType": "User", + "lastModifiedAt": "2023-01-24T02:27:55.6107431+00:00", + "lastModifiedBy": "Microsoft", + "lastModifiedByType": "User" + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-environment-version.json b/tools/c7n_azure/tests_azure/templates/machine-learning-environment-version.json new file mode 100644 index 00000000000..c54c89bbbe9 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-environment-version.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + }, + "workspaceName": { + "type": "string", + "defaultValue": "cctestmlworkspace" + }, + "environmentName": { + "type": "string", + "defaultValue": "cctest-env" + }, + "archivedEnvironmentName": { + "type": "string", + "defaultValue": "cctest-archived-env" + } + }, + "variables": { + "workspaceName": "[concat(parameters('workspaceName'), parameters('uniqueId'))]", + "storageAccountName": "[concat('cctestml', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestmlkv', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestmlai', parameters('uniqueId'))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-09-01", + "name": "[variables('storageAccountName')]", + "location": "eastus", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-10-01", + "name": "[variables('keyVaultName')]", + "location": "eastus", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [] + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "eastus", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "eastus", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}/{2}', variables('workspaceName'), parameters('environmentName'), '1')]", + "properties": { + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "isArchived": false + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}/{2}', variables('workspaceName'), parameters('environmentName'), '2')]", + "properties": { + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "isArchived": true + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]", + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), parameters('environmentName'), '1')]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}/{2}', variables('workspaceName'), parameters('archivedEnvironmentName'), '1')]", + "properties": { + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest", + "isArchived": false + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), parameters('archivedEnvironmentName'))]", + "properties": { + "isArchived": true + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), parameters('archivedEnvironmentName'), '1')]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_environment_version.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_environment_version.py new file mode 100644 index 00000000000..a8f2e876e0f --- /dev/null +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_environment_version.py @@ -0,0 +1,171 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import Mock + +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.mgmt.machinelearningservices.models import ( + EnvironmentVersion, + EnvironmentVersionProperties, +) + +from ..azure_common import BaseTest, arm_template, cassette_name + +RESOURCE_GROUP = 'test_machine-learning-environment-version' + + +def only_ours(resources): + return [r for r in resources if f'/resourcegroups/{RESOURCE_GROUP}/' in r['id'].lower()] + + +def environment_and_version(resource): + environment, version = resource['id'].split('/environments/')[1].split('/versions/') + return environment, version + + +class MachineLearningEnvironmentVersionTest(BaseTest): + + def test_machine_learning_environment_version_schema_validate(self): + with self.sign_out_patch(): + p = self.load_policy({ + 'name': 'find-all-machine-learning-environment-versions', + 'resource': 'azure.machine-learning-environment-version', + }, validate=True) + assert p + + @arm_template('machine-learning-environment-version.json') + @cassette_name('machine-learning-environment-versions') + def test_machine_learning_environment_version_query(self): + p = self.load_policy({ + 'name': 'find-user-machine-learning-environment-versions', + 'resource': 'azure.machine-learning-environment-version', + 'filters': [ + {'type': 'value', 'key': 'properties.environmentType', 'value': 'UserCreated'}, + ], + }) + resources = only_ours(p.run()) + assert {environment_and_version(r) for r in resources} == { + ('cctest-env', '1'), + ('cctest-env', '2'), + ('cctest-archived-env', '1'), + } + assert all('c7n:parent-id' in r for r in resources) + + @arm_template('machine-learning-environment-version.json') + @cassette_name('machine-learning-environment-versions') + def test_machine_learning_environment_version_filter_archived(self): + p = self.load_policy({ + 'name': 'find-archived-machine-learning-environment-versions', + 'resource': 'azure.machine-learning-environment-version', + 'filters': [ + {'type': 'value', 'key': 'properties.environmentType', 'value': 'UserCreated'}, + {'type': 'value', 'key': 'properties.isArchived', 'value': True}, + ], + }) + resources = only_ours(p.run()) + assert [environment_and_version(r) for r in resources] == [('cctest-env', '2')] + + def test_machine_learning_environment_version_skips_missing_versions(self): + parent_id = ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV' + '/providers/Microsoft.MachineLearningServices/workspaces/vvmlwrkspc' + ) + version_id = f'{parent_id}/environments/cctest-env/versions/2' + + version = EnvironmentVersion( + properties=EnvironmentVersionProperties( + image='mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest', + is_archived=True + ) + ) + version.id = version_id + version.name = '2' + + curated, user_created = Mock(), Mock() + curated.name = 'AzureML-Triton' + user_created.name = 'cctest-env' + + def list_versions(name, **kwargs): + if name == 'AzureML-Triton': + raise ResourceNotFoundError('Not Found') + return [version] + + client = Mock() + client.environment_containers.list.return_value = [curated, user_created] + client.environment_versions.list.side_effect = list_versions + + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [{ + 'id': parent_id, + 'name': 'vvmlwrkspc', + 'resourceGroup': 'VV' + }] + + p = self.load_policy({ + 'name': 'find-all-machine-learning-environment-versions', + 'resource': 'azure.machine-learning-environment-version' + }) + manager = p.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + manager.get_client = Mock(return_value=client) + + resources = manager.resources() + + assert [r['id'] for r in resources] == [version_id] + assert resources[0]['properties']['isArchived'] is True + assert resources[0]['c7n:parent-id'] == parent_id + client.environment_containers.list.assert_called_once_with( + resource_group_name='VV', + workspace_name='vvmlwrkspc', + list_view_type='All' + ) + client.environment_versions.list.assert_any_call( + resource_group_name='VV', + workspace_name='vvmlwrkspc', + name='cctest-env', + list_view_type='All' + ) + + def test_machine_learning_environment_version_skips_missing_workspace(self): + client = Mock() + client.environment_containers.list.side_effect = ResourceNotFoundError( + 'ResourceGroupNotFound') + + manager = self._manager_with_client(client) + + assert manager.resources() == [] + + def test_machine_learning_environment_version_raises_other_errors(self): + user_created = Mock() + user_created.name = 'cctest-env' + + client = Mock() + client.environment_containers.list.return_value = [user_created] + client.environment_versions.list.side_effect = HttpResponseError('Forbidden') + + manager = self._manager_with_client(client) + + with self.assertRaises(HttpResponseError): + manager.resources() + + def _manager_with_client(self, client): + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [{ + 'id': ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV' + '/providers/Microsoft.MachineLearningServices/workspaces/vvmlwrkspc' + ), + 'name': 'vvmlwrkspc', + 'resourceGroup': 'VV' + }] + + p = self.load_policy({ + 'name': 'find-all-machine-learning-environment-versions', + 'resource': 'azure.machine-learning-environment-version' + }) + manager = p.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + manager.get_client = Mock(return_value=client) + return manager From 0249e6271c9cc0de445a6ca2009be83c4f8551cf Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:23:59 -0500 Subject: [PATCH 39/65] aws - cloudtrail - add set-event-selectors action (#11195) --------- Co-authored-by: dreakj12 --- c7n/resources/cloudtrail.py | 120 +++++++++- .../cloudtrail.DescribeTrails_1.json | 60 +++++ .../cloudtrail.DescribeTrails_2.json | 60 +++++ .../cloudtrail.GetEventSelectors_1.json | 15 ++ .../cloudtrail.GetEventSelectors_2.json | 43 ++++ .../cloudtrail.GetEventSelectors_3.json | 43 ++++ .../cloudtrail.PutEventSelectors_1.json | 43 ++++ .../tagging.GetResources_1.json | 8 + .../tagging.GetResources_2.json | 8 + .../cloudtrail.DescribeTrails_1.json | 60 +++++ .../cloudtrail.DescribeTrails_2.json | 60 +++++ .../cloudtrail.GetEventSelectors_1.json | 43 ++++ .../cloudtrail.GetEventSelectors_2.json | 22 ++ .../cloudtrail.PutEventSelectors_1.json | 43 ++++ .../cloudtrail.PutEventSelectors_2.json | 22 ++ .../tagging.GetResources_1.json | 8 + .../tagging.GetResources_2.json | 8 + .../cloudtrail.DescribeTrails_1.json | 60 +++++ .../cloudtrail.PutEventSelectors_1.json | 10 + .../cloudtrail.PutEventSelectors_2.json | 10 + .../tagging.GetResources_1.json | 8 + .../cloudtrail.DescribeTrails_1.json | 60 +++++ .../cloudtrail.DescribeTrails_2.json | 60 +++++ .../cloudtrail.GetEventSelectors_1.json | 15 ++ .../cloudtrail.GetEventSelectors_2.json | 43 ++++ .../cloudtrail.PutEventSelectors_1.json | 43 ++++ .../cloudtrail.PutEventSelectors_2.json | 43 ++++ .../tagging.GetResources_1.json | 8 + .../tagging.GetResources_2.json | 8 + .../cloudtrail.PutEventSelectors_1.json | 10 + .../cloudtrail_set_event_selectors/main.tf | 104 +++++++++ .../tf_resources.json | 210 ++++++++++++++++++ tests/test_cloudtrail.py | 187 ++++++++++++++++ 33 files changed, 1544 insertions(+), 1 deletion(-) create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_3.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.PutEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.DescribeTrails_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_error/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_2.json create mode 100644 tests/data/placebo/test_cloudtrail_set_event_selectors_not_found/cloudtrail.PutEventSelectors_1.json create mode 100644 tests/terraform/cloudtrail_set_event_selectors/main.tf create mode 100644 tests/terraform/cloudtrail_set_event_selectors/tf_resources.json diff --git a/c7n/resources/cloudtrail.py b/c7n/resources/cloudtrail.py index 81b282f0e42..2b6a0c8b338 100644 --- a/c7n/resources/cloudtrail.py +++ b/c7n/resources/cloudtrail.py @@ -2,8 +2,10 @@ # SPDX-License-Identifier: Apache-2.0 import logging +from botocore.exceptions import ClientError + from c7n.actions import Action, BaseAction -from c7n.exceptions import PolicyValidationError +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from c7n.filters import ValueFilter, Filter from c7n.manager import resources from c7n.tags import universal_augment @@ -208,6 +210,122 @@ def process(self, resources): **self.data['attributes']) +@CloudTrail.action_registry.register('set-event-selectors') +class SetEventSelectors(Action): + """Set the event selectors of a trail. + + Specify either ``event-selectors`` (basic) or + ``advanced-event-selectors``, not both. + + This replaces all of the trail's existing selectors, of either type, + so include every selector the trail should keep. When pairing this + action with the ``event-selectors`` filter, filter on the same + selector type the action writes; otherwise the filter keeps matching + and the policy re-applies the change on every run. + + See the `PutEventSelectors API reference + `__ + for the syntax of both selector types, and `Logging data events + `__ + for a detailed comparison and examples. + + :Example: + + .. code-block:: yaml + + policies: + - name: cloudtrail-log-s3-writes + resource: aws.cloudtrail + filters: + - type: is-shadow + state: false + - type: event-selectors + key: AdvancedEventSelectors + value: empty + actions: + - type: set-event-selectors + advanced-event-selectors: + - Name: Log all management events + FieldSelectors: + - Field: eventCategory + Equals: [Management] + - Name: Log S3 object writes + FieldSelectors: + - Field: eventCategory + Equals: [Data] + - Field: resources.type + Equals: [AWS::S3::Object] + - Field: readOnly + Equals: ["false"] + """ + schema = type_schema( + 'set-event-selectors', + **{ + 'event-selectors': { + 'type': 'array', 'items': {'type': 'object'}, 'minItems': 1}, + 'advanced-event-selectors': { + 'type': 'array', 'items': {'type': 'object'}, 'minItems': 1}, + }) + schema['oneOf'] = [ + {'required': ['event-selectors']}, + {'required': ['advanced-event-selectors']}, + ] + shape = 'PutEventSelectorsRequest' + permissions = ('cloudtrail:PutEventSelectors',) + + def get_params(self): + params = {} + if 'event-selectors' in self.data: + params['EventSelectors'] = self.data['event-selectors'] + if 'advanced-event-selectors' in self.data: + params['AdvancedEventSelectors'] = self.data['advanced-event-selectors'] + return params + + def validate(self): + # mirrors the schema's oneOf/minItems, which only applies when + # schema validation is enabled + params = self.get_params() + if len(params) != 1 or not all(params.values()): + raise PolicyValidationError( + "set-event-selectors requires exactly one non-empty list of " + "event-selectors or advanced-event-selectors on %s" % ( + self.manager.data,)) + params['TrailName'] = 'PolicyValidation' + return shape_validate( + params, + self.shape, + self.manager.resource_type.service) + + def process(self, resources): + shadow_check = IsShadow({'state': False}, self.manager) + shadow_check.embedded = True + resources = shadow_check.process(resources) + params = self.get_params() + + errors = [] + grouped_trails = get_trail_groups(self.manager.session_factory, resources) + for region, (client, trails) in grouped_trails.items(): + for t in trails: + try: + self.manager.retry( + client.put_event_selectors, TrailName=t['TrailARN'], **params) + except client.exceptions.TrailNotFoundException: + self.log.warning( + "trail %s no longer exists, skipping", t['TrailARN']) + continue + except ClientError as e: + self.log.error( + "failed to set event selectors on %s: %s", t['TrailARN'], e) + errors.append(t['TrailARN']) + continue + # drop any stale filter annotation + t.pop(EventSelectors.annotation_key, None) + if errors: + raise PolicyExecutionError( + "set-event-selectors failed on %d trail(s): %s" % ( + len(errors), ", ".join(errors))) + + @CloudTrail.action_registry.register('set-logging') class SetLogging(Action): """Set the logging state of a trail diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_1.json new file mode 100644 index 00000000000..e48cf4d424f --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_1.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_2.json new file mode 100644 index 00000000000..7cae714e79c --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.DescribeTrails_2.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_1.json new file mode 100644 index 00000000000..91234e5c1cc --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_1.json @@ -0,0 +1,15 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "EventSelectors": [ + { + "ReadWriteType": "All", + "IncludeManagementEvents": true, + "DataResources": [], + "ExcludeManagementEventSources": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_2.json new file mode 100644 index 00000000000..ece570b9bd9 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_2.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_3.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_3.json new file mode 100644 index 00000000000..ece570b9bd9 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.GetEventSelectors_3.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.PutEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.PutEventSelectors_1.json new file mode 100644 index 00000000000..7f52a4e1d15 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/cloudtrail.PutEventSelectors_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_1.json new file mode 100644 index 00000000000..7cae714e79c --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_1.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_2.json new file mode 100644 index 00000000000..50d71dad58d --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.DescribeTrails_2.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_1.json new file mode 100644 index 00000000000..ec67378fb74 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_2.json new file mode 100644 index 00000000000..5c8a3cabb4e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.GetEventSelectors_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "EventSelectors": [ + { + "ReadWriteType": "WriteOnly", + "IncludeManagementEvents": true, + "DataResources": [ + { + "Type": "AWS::S3::Object", + "Values": [ + "arn:aws:s3:::c7n-selectors9e65b1d1317aca1c22ad67cdf3/" + ] + } + ], + "ExcludeManagementEventSources": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_1.json new file mode 100644 index 00000000000..388b3539005 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_2.json new file mode 100644 index 00000000000..5c8a3cabb4e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/cloudtrail.PutEventSelectors_2.json @@ -0,0 +1,22 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "EventSelectors": [ + { + "ReadWriteType": "WriteOnly", + "IncludeManagementEvents": true, + "DataResources": [ + { + "Type": "AWS::S3::Object", + "Values": [ + "arn:aws:s3:::c7n-selectors9e65b1d1317aca1c22ad67cdf3/" + ] + } + ], + "ExcludeManagementEventSources": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_basic/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.DescribeTrails_1.json new file mode 100644 index 00000000000..ffd3419122e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.DescribeTrails_1.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_1.json new file mode 100644 index 00000000000..3c43ac736a7 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 400, + "data": { + "Error": { + "Message": "The following fields are required: [eventCategory].", + "Code": "InvalidEventSelectorsException" + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_2.json new file mode 100644 index 00000000000..3c43ac736a7 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/cloudtrail.PutEventSelectors_2.json @@ -0,0 +1,10 @@ +{ + "status_code": 400, + "data": { + "Error": { + "Message": "The following fields are required: [eventCategory].", + "Code": "InvalidEventSelectorsException" + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_error/tagging.GetResources_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_error/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_1.json new file mode 100644 index 00000000000..50d71dad58d --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_1.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": false, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_2.json new file mode 100644 index 00000000000..ffd3419122e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.DescribeTrails_2.json @@ -0,0 +1,60 @@ +{ + "status_code": 200, + "data": { + "trailList": [ + { + "Name": "c7n-selectors-42695-advanced", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-basic", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "c7n-selectors-42695-mismatch", + "S3BucketName": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "IncludeGlobalServiceEvents": false, + "IsMultiRegionTrail": false, + "HomeRegion": "us-east-1", + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + }, + { + "Name": "management-events", + "S3BucketName": "aws-cloudtrail-logs-644160558196-d55943b8", + "IncludeGlobalServiceEvents": true, + "IsMultiRegionTrail": true, + "HomeRegion": "us-east-2", + "TrailARN": "arn:aws:cloudtrail:us-east-2:644160558196:trail/management-events", + "LogFileValidationEnabled": false, + "HasCustomEventSelectors": true, + "HasInsightSelectors": false, + "IsOrganizationTrail": false, + "RecursiveLogging": true + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_1.json new file mode 100644 index 00000000000..f09b50768bd --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_1.json @@ -0,0 +1,15 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "EventSelectors": [ + { + "ReadWriteType": "All", + "IncludeManagementEvents": true, + "DataResources": [], + "ExcludeManagementEventSources": [] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_2.json new file mode 100644 index 00000000000..585bf890f53 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.GetEventSelectors_2.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_1.json new file mode 100644 index 00000000000..a8c6ea7d15e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_1.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_2.json new file mode 100644 index 00000000000..a8c6ea7d15e --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/cloudtrail.PutEventSelectors_2.json @@ -0,0 +1,43 @@ +{ + "status_code": 200, + "data": { + "TrailARN": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "AdvancedEventSelectors": [ + { + "Name": "Log all management events", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Management" + ] + } + ] + }, + { + "Name": "Log S3 object writes", + "FieldSelectors": [ + { + "Field": "eventCategory", + "Equals": [ + "Data" + ] + }, + { + "Field": "resources.type", + "Equals": [ + "AWS::S3::Object" + ] + }, + { + "Field": "readOnly", + "Equals": [ + "false" + ] + } + ] + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_2.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_2.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_mismatch/tagging.GetResources_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_cloudtrail_set_event_selectors_not_found/cloudtrail.PutEventSelectors_1.json b/tests/data/placebo/test_cloudtrail_set_event_selectors_not_found/cloudtrail.PutEventSelectors_1.json new file mode 100644 index 00000000000..668e20dff09 --- /dev/null +++ b/tests/data/placebo/test_cloudtrail_set_event_selectors_not_found/cloudtrail.PutEventSelectors_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 400, + "data": { + "Error": { + "Message": "Unknown trail: arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced-missing for the user: 644160558196", + "Code": "TrailNotFoundException" + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/cloudtrail_set_event_selectors/main.tf b/tests/terraform/cloudtrail_set_event_selectors/main.tf new file mode 100644 index 00000000000..2ca9cd88416 --- /dev/null +++ b/tests/terraform/cloudtrail_set_event_selectors/main.tf @@ -0,0 +1,104 @@ +provider "aws" { + region = "us-east-1" +} + +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +resource "random_integer" "trail" { + min = 1 + max = 50000 +} + +# one trail per test so the tests stay order independent +locals { + prefix = "c7n-selectors-${random_integer.trail.id}" + trail_arn = "arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${local.prefix}" + trail_arns = [for t in ["advanced", "basic", "mismatch"] : "${local.trail_arn}-${t}"] +} + +resource "aws_s3_bucket" "trail" { + bucket_prefix = "c7n-selectors" + force_destroy = true +} + +resource "aws_s3_bucket_public_access_block" "trail" { + bucket = aws_s3_bucket.trail.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "trail" { + bucket = aws_s3_bucket.trail.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AWSCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.trail.arn + Condition = { StringEquals = { "AWS:SourceArn" = local.trail_arns } } + }, + { + Sid = "AWSCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.trail.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = local.trail_arns + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + } + ] + }) +} + +resource "aws_cloudtrail" "advanced" { + name = "${local.prefix}-advanced" + s3_bucket_name = aws_s3_bucket.trail.bucket + include_global_service_events = false + enable_logging = false + + event_selector { + read_write_type = "All" + include_management_events = true + } + + depends_on = [aws_s3_bucket_policy.trail] +} + +resource "aws_cloudtrail" "basic" { + name = "${local.prefix}-basic" + s3_bucket_name = aws_s3_bucket.trail.bucket + include_global_service_events = false + enable_logging = false + + event_selector { + read_write_type = "All" + include_management_events = true + } + + depends_on = [aws_s3_bucket_policy.trail] +} + +resource "aws_cloudtrail" "mismatch" { + name = "${local.prefix}-mismatch" + s3_bucket_name = aws_s3_bucket.trail.bucket + include_global_service_events = false + enable_logging = false + + event_selector { + read_write_type = "All" + include_management_events = true + } + + depends_on = [aws_s3_bucket_policy.trail] +} diff --git a/tests/terraform/cloudtrail_set_event_selectors/tf_resources.json b/tests/terraform/cloudtrail_set_event_selectors/tf_resources.json new file mode 100644 index 00000000000..94ab240e620 --- /dev/null +++ b/tests/terraform/cloudtrail_set_event_selectors/tf_resources.json @@ -0,0 +1,210 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_caller_identity": { + "current": { + "account_id": "644160558196", + "arn": "arn:aws:sts::644160558196:assumed-role/c7n-test-role/c7n-test-user", + "id": "644160558196", + "user_id": "AROAEXAMPLEROLEID0000:c7n-test-user" + } + }, + "aws_region": { + "current": { + "description": "US East (N. Virginia)", + "endpoint": "ec2.us-east-1.amazonaws.com", + "id": "us-east-1", + "name": "us-east-1", + "region": "us-east-1" + } + }, + "aws_cloudtrail": { + "advanced": { + "advanced_event_selector": [], + "arn": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "cloud_watch_logs_group_arn": "", + "cloud_watch_logs_role_arn": "", + "enable_log_file_validation": false, + "enable_logging": false, + "event_selector": [ + { + "data_resource": [], + "exclude_management_event_sources": null, + "include_management_events": true, + "read_write_type": "All" + } + ], + "home_region": "us-east-1", + "id": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced", + "include_global_service_events": false, + "insight_selector": [], + "is_multi_region_trail": false, + "is_organization_trail": false, + "kms_key_id": "", + "name": "c7n-selectors-42695-advanced", + "region": "us-east-1", + "s3_bucket_name": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "s3_key_prefix": "", + "sns_topic_arn": "", + "sns_topic_name": "", + "tags": null, + "tags_all": {} + }, + "basic": { + "advanced_event_selector": [], + "arn": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "cloud_watch_logs_group_arn": "", + "cloud_watch_logs_role_arn": "", + "enable_log_file_validation": false, + "enable_logging": false, + "event_selector": [ + { + "data_resource": [], + "exclude_management_event_sources": null, + "include_management_events": true, + "read_write_type": "All" + } + ], + "home_region": "us-east-1", + "id": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic", + "include_global_service_events": false, + "insight_selector": [], + "is_multi_region_trail": false, + "is_organization_trail": false, + "kms_key_id": "", + "name": "c7n-selectors-42695-basic", + "region": "us-east-1", + "s3_bucket_name": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "s3_key_prefix": "", + "sns_topic_arn": "", + "sns_topic_name": "", + "tags": null, + "tags_all": {} + }, + "mismatch": { + "advanced_event_selector": [], + "arn": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "cloud_watch_logs_group_arn": "", + "cloud_watch_logs_role_arn": "", + "enable_log_file_validation": false, + "enable_logging": false, + "event_selector": [ + { + "data_resource": [], + "exclude_management_event_sources": null, + "include_management_events": true, + "read_write_type": "All" + } + ], + "home_region": "us-east-1", + "id": "arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch", + "include_global_service_events": false, + "insight_selector": [], + "is_multi_region_trail": false, + "is_organization_trail": false, + "kms_key_id": "", + "name": "c7n-selectors-42695-mismatch", + "region": "us-east-1", + "s3_bucket_name": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "s3_key_prefix": "", + "sns_topic_arn": "", + "sns_topic_name": "", + "tags": null, + "tags_all": {} + } + }, + "aws_s3_bucket": { + "trail": { + "acceleration_status": "", + "acl": null, + "arn": "arn:aws:s3:::c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "bucket": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "bucket_domain_name": "c7n-selectors9e65b1d1317aca1c22ad67cdf3.s3.amazonaws.com", + "bucket_namespace": "global", + "bucket_prefix": "c7n-selectors", + "bucket_region": "us-east-1", + "bucket_regional_domain_name": "c7n-selectors9e65b1d1317aca1c22ad67cdf3.s3.us-east-1.amazonaws.com", + "cors_rule": [], + "force_destroy": true, + "grant": [ + { + "id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "permissions": [ + "FULL_CONTROL" + ], + "type": "CanonicalUser", + "uri": "" + } + ], + "hosted_zone_id": "Z3AQBSTGFYJSTF", + "id": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "lifecycle_rule": [], + "logging": [], + "object_lock_configuration": [], + "object_lock_enabled": false, + "policy": "", + "region": "us-east-1", + "replication_configuration": [], + "request_payer": "BucketOwner", + "server_side_encryption_configuration": [ + { + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "kms_master_key_id": "", + "sse_algorithm": "AES256" + } + ], + "bucket_key_enabled": false + } + ] + } + ], + "tags": null, + "tags_all": {}, + "timeouts": null, + "versioning": [ + { + "enabled": false, + "mfa_delete": false + } + ], + "website": [], + "website_domain": null, + "website_endpoint": null + } + }, + "aws_s3_bucket_policy": { + "trail": { + "bucket": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "id": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "policy": "{\"Statement\":[{\"Action\":\"s3:GetBucketAcl\",\"Condition\":{\"StringEquals\":{\"AWS:SourceArn\":[\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced\",\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic\",\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch\"]}},\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudtrail.amazonaws.com\"},\"Resource\":\"arn:aws:s3:::c7n-selectors9e65b1d1317aca1c22ad67cdf3\",\"Sid\":\"AWSCloudTrailAclCheck\"},{\"Action\":\"s3:PutObject\",\"Condition\":{\"StringEquals\":{\"AWS:SourceArn\":[\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-advanced\",\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-basic\",\"arn:aws:cloudtrail:us-east-1:644160558196:trail/c7n-selectors-42695-mismatch\"],\"s3:x-amz-acl\":\"bucket-owner-full-control\"}},\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"cloudtrail.amazonaws.com\"},\"Resource\":\"arn:aws:s3:::c7n-selectors9e65b1d1317aca1c22ad67cdf3/*\",\"Sid\":\"AWSCloudTrailWrite\"}],\"Version\":\"2012-10-17\"}", + "region": "us-east-1" + } + }, + "aws_s3_bucket_public_access_block": { + "trail": { + "block_public_acls": true, + "block_public_policy": true, + "bucket": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "id": "c7n-selectors9e65b1d1317aca1c22ad67cdf3", + "ignore_public_acls": true, + "region": "us-east-1", + "restrict_public_buckets": true, + "skip_destroy": null + } + }, + "random_integer": { + "trail": { + "id": "42695", + "keepers": null, + "max": 50000, + "min": 1, + "result": 42695, + "seed": null + } + } + } +} \ No newline at end of file diff --git a/tests/test_cloudtrail.py b/tests/test_cloudtrail.py index 0dfa5b5bb32..6aa4c63d0dc 100644 --- a/tests/test_cloudtrail.py +++ b/tests/test_cloudtrail.py @@ -1,6 +1,12 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import logging import time + +import pytest +from pytest_terraform import terraform + +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from .common import BaseTest @@ -192,3 +198,184 @@ def test_cloudtrail_delete(self): client.exceptions.TrailNotFoundException, client.delete_trail, Name=resources[0]['Name']) + + +ADVANCED_SELECTORS = [ + { + 'Name': 'Log all management events', + 'FieldSelectors': [ + {'Field': 'eventCategory', 'Equals': ['Management']}, + ], + }, + { + 'Name': 'Log S3 object writes', + 'FieldSelectors': [ + {'Field': 'eventCategory', 'Equals': ['Data']}, + {'Field': 'resources.type', 'Equals': ['AWS::S3::Object']}, + {'Field': 'readOnly', 'Equals': ['false']}, + ], + }, +] + + +def set_selectors_policy(test, factory, trail_name, filters, action): + return test.load_policy( + { + 'name': 'cloudtrail-set-event-selectors', + 'resource': 'aws.cloudtrail', + 'filters': [{'Name': trail_name}] + filters, + 'actions': [dict(action, type='set-event-selectors')], + }, + session_factory=factory, + ) + + +@terraform('cloudtrail_set_event_selectors', scope='session') +def test_cloudtrail_set_event_selectors(test, cloudtrail_set_event_selectors): + """Basic -> advanced replaces the basic selectors, and a filter on the + same selector type stops matching once the action has run.""" + factory = test.replay_flight_data('test_cloudtrail_set_event_selectors') + trail_name = cloudtrail_set_event_selectors['aws_cloudtrail.advanced.name'] + policy = set_selectors_policy( + test, factory, trail_name, + [{'type': 'event-selectors', 'key': 'AdvancedEventSelectors', 'value': 'empty'}], + {'advanced-event-selectors': ADVANCED_SELECTORS}) + + resources = policy.run() + + assert [r['Name'] for r in resources] == [trail_name] + client = factory().client('cloudtrail') + selectors = client.get_event_selectors(TrailName=trail_name) + assert not selectors.get('EventSelectors') + assert [s['Name'] for s in selectors['AdvancedEventSelectors']] == [ + s['Name'] for s in ADVANCED_SELECTORS] + + assert policy.run() == [] + + +@terraform('cloudtrail_set_event_selectors', scope='session') +def test_cloudtrail_set_event_selectors_basic(test, cloudtrail_set_event_selectors): + """Advanced -> basic replaces the advanced selectors.""" + factory = test.replay_flight_data('test_cloudtrail_set_event_selectors_basic') + trail_name = cloudtrail_set_event_selectors['aws_cloudtrail.basic.name'] + basic_selectors = [{ + 'ReadWriteType': 'WriteOnly', + 'IncludeManagementEvents': True, + 'DataResources': [{ + 'Type': 'AWS::S3::Object', + 'Values': [cloudtrail_set_event_selectors['aws_s3_bucket.trail.arn'] + '/'], + }], + }] + set_selectors_policy( + test, factory, trail_name, [], + {'advanced-event-selectors': ADVANCED_SELECTORS}).run() + policy = set_selectors_policy( + test, factory, trail_name, + [{'type': 'event-selectors', 'key': 'EventSelectors', 'value': 'empty'}], + {'event-selectors': basic_selectors}) + + resources = policy.run() + + assert [r['Name'] for r in resources] == [trail_name] + client = factory().client('cloudtrail') + selectors = client.get_event_selectors(TrailName=trail_name) + assert not selectors.get('AdvancedEventSelectors') + assert len(selectors['EventSelectors']) == 1 + selector = selectors['EventSelectors'][0] + assert selector['ReadWriteType'] == 'WriteOnly' + assert selector['DataResources'] == basic_selectors[0]['DataResources'] + + +@terraform('cloudtrail_set_event_selectors', scope='session') +def test_cloudtrail_set_event_selectors_mismatch(test, cloudtrail_set_event_selectors): + """A filter on basic selectors paired with an action writing advanced + selectors keeps matching, so the policy re-applies on every run.""" + factory = test.replay_flight_data('test_cloudtrail_set_event_selectors_mismatch') + trail_name = cloudtrail_set_event_selectors['aws_cloudtrail.mismatch.name'] + policy = set_selectors_policy( + test, factory, trail_name, + [{ + 'type': 'event-selectors', + 'key': "EventSelectors[?DataResources[?Type=='AWS::S3::Object']] | []", + 'value': 'empty', + }], + {'advanced-event-selectors': ADVANCED_SELECTORS}) + + assert [r['Name'] for r in policy.run()] == [trail_name] + assert [r['Name'] for r in policy.run()] == [trail_name] + + +@terraform('cloudtrail_set_event_selectors', scope='session') +def test_cloudtrail_set_event_selectors_not_found(test, cloudtrail_set_event_selectors, caplog): + """A trail deleted after it was described is skipped, not an error.""" + factory = test.replay_flight_data('test_cloudtrail_set_event_selectors_not_found') + trail_name = cloudtrail_set_event_selectors['aws_cloudtrail.advanced.name'] + policy = set_selectors_policy( + test, factory, trail_name, [], + {'advanced-event-selectors': ADVANCED_SELECTORS}) + missing_arn = cloudtrail_set_event_selectors['aws_cloudtrail.advanced.arn'] + '-missing' + + with caplog.at_level(logging.WARNING): + policy.resource_manager.actions[0].process( + [{'Name': trail_name + '-missing', 'TrailARN': missing_arn}]) + + assert f"trail {missing_arn} no longer exists" in caplog.text + + +@terraform('cloudtrail_set_event_selectors', scope='session') +def test_cloudtrail_set_event_selectors_error(test, cloudtrail_set_event_selectors): + """A rejected trail doesn't stop the remaining trails from being attempted, + and the failures are raised once every trail has been tried.""" + factory = test.replay_flight_data('test_cloudtrail_set_event_selectors_error') + trail_names = [ + cloudtrail_set_event_selectors['aws_cloudtrail.advanced.name'], + cloudtrail_set_event_selectors['aws_cloudtrail.basic.name'], + ] + # well formed, so it passes shape validation, but AWS rejects the field + policy = test.load_policy( + { + 'name': 'cloudtrail-set-event-selectors', + 'resource': 'aws.cloudtrail', + 'filters': [{'type': 'value', 'key': 'Name', 'op': 'in', 'value': trail_names}], + 'actions': [{ + 'type': 'set-event-selectors', + 'advanced-event-selectors': [{ + 'Name': 'Invalid field', + 'FieldSelectors': [{'Field': 'c7nInvalidField', 'Equals': ['x']}], + }], + }], + }, + session_factory=factory, + ) + + with pytest.raises(PolicyExecutionError) as e: + policy.run() + + assert "failed on 2 trail(s)" in str(e.value) + for name in trail_names: + assert f"trail/{name}" in str(e.value) + + +@pytest.mark.parametrize('action', [ + {'type': 'set-event-selectors'}, + { + 'type': 'set-event-selectors', + 'event-selectors': [{'ReadWriteType': 'All'}], + 'advanced-event-selectors': [ + {'FieldSelectors': [{'Field': 'eventCategory', 'Equals': ['Management']}]}], + }, + { + 'type': 'set-event-selectors', + 'event-selectors': [{'IncludeManagementEvents': 'yes'}], + }, + {'type': 'set-event-selectors', 'event-selectors': []}, + {'type': 'set-event-selectors', 'advanced-event-selectors': []}, +]) +@pytest.mark.parametrize('validate', [False, True]) +def test_cloudtrail_set_event_selectors_validation(test, action, validate): + with pytest.raises(PolicyValidationError): + test.load_policy({ + 'name': 'cloudtrail-set-event-selectors', + 'resource': 'aws.cloudtrail', + 'actions': [action], + }, validate=validate) From 9464343b40239f890a936dd56f529a845c68c355 Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:34:59 -0500 Subject: [PATCH 40/65] gcp - loadbalancer-target-https-proxy - add ssl-policy filter (#11163) --- .../c7n_gcp/c7n_gcp/resources/loadbalancer.py | 93 +++++ ...l-sslPolicies-c7n-default-aa7f-good_1.json | 39 ++ ...custodian-global-targetHttpsProxies_1.json | 84 +++++ ...custodian-global-targetHttpsProxies_1.json | 84 +++++ ...custodian-global-targetHttpsProxies_1.json | 84 +++++ ...l-sslPolicies-c7n-default-aa7f-weak_1.json | 44 +++ ...custodian-global-targetHttpsProxies_1.json | 84 +++++ ...l-sslPolicies-c7n-default-3e3a-weak_1.json | 44 +++ ...d-custodian-global-targetSslProxies_1.json | 38 ++ .../lb_https_proxy_ssl_policy/main.tf | 83 +++++ .../tf_resources.json | 345 ++++++++++++++++++ .../terraform/lb_ssl_proxy_ssl_policy/main.tf | 55 +++ .../lb_ssl_proxy_ssl_policy/tf_resources.json | 235 ++++++++++++ tools/c7n_gcp/tests/test_loadbalancer.py | 94 +++++ 14 files changed, 1406 insertions(+) create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-good_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-not-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-weak_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-3e3a-weak_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetSslProxies_1.json create mode 100644 tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/tf_resources.json create mode 100644 tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/loadbalancer.py b/tools/c7n_gcp/c7n_gcp/resources/loadbalancer.py index 09be2b83c90..414c705433e 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/loadbalancer.py +++ b/tools/c7n_gcp/c7n_gcp/resources/loadbalancer.py @@ -1,12 +1,18 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import logging import re +from googleapiclient.errors import HttpError + +from c7n.filters.core import ValueFilter from c7n.utils import type_schema, local_session from c7n_gcp.actions import MethodAction from c7n_gcp.provider import resources from c7n_gcp.query import QueryResourceManager, TypeInfo +log = logging.getLogger('custodian.gcp.loadbalancer') + @resources.register('loadbalancer-address') class LoadBalancingAddress(QueryResourceManager): @@ -273,6 +279,93 @@ def get(client, resource_info): 'resourceName'].rsplit('/', 1)[-1]}) +@LoadBalancingTargetHttpsProxy.filter_registry.register('ssl-policy') +@LoadBalancingTargetSslProxy.filter_registry.register('ssl-policy') +class TargetProxySslPolicyFilter(ValueFilter): + """Filter target proxies by the SSL policy attached to them. + + The SSL policy named in the proxy's ``sslPolicy`` field is fetched and + the value filter is applied to it. A proxy with no ``sslPolicy`` does + not match this filter. Such a proxy uses the GCP default SSL policy, + which can be found with a ``sslPolicy: absent`` value filter. + + Note that inside a ``not`` block a proxy with no ``sslPolicy`` fails + every ``ssl-policy`` check, so the ``not`` makes it match. + + https://cloud.google.com/compute/docs/reference/rest/v1/sslPolicies + + :example: + + Find HTTPS proxies whose SSL policy uses the COMPATIBLE profile + + .. code-block:: yaml + + policies: + - name: https-proxy-weak-ssl-policy + resource: gcp.loadbalancer-target-https-proxy + filters: + - type: ssl-policy + key: profile + value: COMPATIBLE + + :example: + + Find SSL proxies that use the GCP default SSL policy or an SSL policy + that allows TLS versions older than 1.2 + + .. code-block:: yaml + + policies: + - name: ssl-proxy-old-tls + resource: gcp.loadbalancer-target-ssl-proxy + filters: + - or: + - type: value + key: sslPolicy + value: absent + - type: ssl-policy + key: minTlsVersion + op: ne + value: TLS_1_2 + """ + + schema = type_schema('ssl-policy', rinherit=ValueFilter.schema) + permissions = ('compute.sslPolicies.get',) + annotation_key = 'c7n:ssl-policy' + policy_link = re.compile(r'.*projects/([^/]+)/global/sslPolicies/([^/]+)$') + + def __init__(self, data, manager=None): + super().__init__(data, manager) + # ssl policy selfLink -> fetched policy, so proxies sharing a + # policy only fetch it once. + self.ssl_policies = {} + + def __call__(self, proxy): + if self.annotation_key not in proxy: + link = proxy.get('sslPolicy') + if not link: + return False + if link not in self.ssl_policies: + self.ssl_policies[link] = self.get_ssl_policy(link) + if self.ssl_policies[link] is None: + return False + proxy[self.annotation_key] = self.ssl_policies[link] + return super().__call__(proxy[self.annotation_key]) + + def get_ssl_policy(self, link): + project, name = self.policy_link.match(link).groups() + session = local_session(self.manager.session_factory) + client = session.client('compute', 'v1', 'sslPolicies') + try: + return client.execute_command( + 'get', {'project': project, 'sslPolicy': name}) + except HttpError as e: + if e.resp.status != 404: + raise + log.warning("ssl policy not found: %s", link) + return None + + @resources.register('loadbalancer-backend-bucket') class LoadBalancingBackendBucket(QueryResourceManager): """GCP resource: https://cloud.google.com/compute/docs/reference/rest/v1/backendBuckets diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-good_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-good_1.json new file mode 100644 index 00000000000..82eb1fff8fe --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-good_1.json @@ -0,0 +1,39 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:18 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "852", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good?alt=json" + }, + "body": { + "kind": "compute#sslPolicy", + "id": "5584483765961396101", + "creationTimestamp": "2026-09-30T08:57:30.457-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "name": "c7n-default-aa7f-good", + "profile": "MODERN", + "minTlsVersion": "TLS_1_2", + "enabledFeatures": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + ], + "fingerprint": "4JopUddr0yY=" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json new file mode 100644 index 00000000000..c32d73172d5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-good/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json @@ -0,0 +1,84 @@ +{ + "headers": { + "etag": "RDINwBH0aqNvFSiJx2kWyeDCIa4=/M0nEeO2YkxTjlLRCEz6_OPottHg=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:17 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3475", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies?alt=json" + }, + "body": { + "kind": "compute#targetHttpsProxyList", + "id": "projects/cloud-custodian/global/targetHttpsProxies", + "items": [ + { + "kind": "compute#targetHttpsProxy", + "id": "3657098629032742754", + "creationTimestamp": "2026-09-30T08:58:05.527-07:00", + "name": "c7n-default-aa7f-https-good", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "fingerprint": "eWmtHCE-9FE=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2603121230916652898", + "creationTimestamp": "2026-09-30T08:58:05.953-07:00", + "name": "c7n-default-aa7f-https-none", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "fingerprint": "tUieqhM7-us=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "203287616958795618", + "creationTimestamp": "2026-09-30T08:58:05.498-07:00", + "name": "c7n-default-aa7f-https-weak", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2427009597342519138", + "creationTimestamp": "2026-09-30T08:58:05.407-07:00", + "name": "c7n-default-aa7f-https-weak2", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + } + ], + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json new file mode 100644 index 00000000000..157b2e84536 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json @@ -0,0 +1,84 @@ +{ + "headers": { + "etag": "Z1d5e1nLoRQpLvrTxcrhM8rfs04=/bVUXOzslCNwlV9qYOT7Bxr2_kzA=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:18 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3475", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies?alt=json" + }, + "body": { + "kind": "compute#targetHttpsProxyList", + "id": "projects/cloud-custodian/global/targetHttpsProxies", + "items": [ + { + "kind": "compute#targetHttpsProxy", + "id": "3657098629032742754", + "creationTimestamp": "2026-09-30T08:58:05.527-07:00", + "name": "c7n-default-aa7f-https-good", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "fingerprint": "eWmtHCE-9FE=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2603121230916652898", + "creationTimestamp": "2026-09-30T08:58:05.953-07:00", + "name": "c7n-default-aa7f-https-none", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "fingerprint": "tUieqhM7-us=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "203287616958795618", + "creationTimestamp": "2026-09-30T08:58:05.498-07:00", + "name": "c7n-default-aa7f-https-weak", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2427009597342519138", + "creationTimestamp": "2026-09-30T08:58:05.407-07:00", + "name": "c7n-default-aa7f-https-weak2", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + } + ], + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-not-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-not-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json new file mode 100644 index 00000000000..59ada7e65d7 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-not-none/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json @@ -0,0 +1,84 @@ +{ + "headers": { + "etag": "-UUX00qNlp8_hLjXS_RjwbcT7Sk=/W8BO8ltmCCii8bd_x--Oug9WiXc=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:20 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3475", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies?alt=json" + }, + "body": { + "kind": "compute#targetHttpsProxyList", + "id": "projects/cloud-custodian/global/targetHttpsProxies", + "items": [ + { + "kind": "compute#targetHttpsProxy", + "id": "3657098629032742754", + "creationTimestamp": "2026-09-30T08:58:05.527-07:00", + "name": "c7n-default-aa7f-https-good", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "fingerprint": "eWmtHCE-9FE=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2603121230916652898", + "creationTimestamp": "2026-09-30T08:58:05.953-07:00", + "name": "c7n-default-aa7f-https-none", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "fingerprint": "tUieqhM7-us=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "203287616958795618", + "creationTimestamp": "2026-09-30T08:58:05.498-07:00", + "name": "c7n-default-aa7f-https-weak", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2427009597342519138", + "creationTimestamp": "2026-09-30T08:58:05.407-07:00", + "name": "c7n-default-aa7f-https-weak2", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + } + ], + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-weak_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-weak_1.json new file mode 100644 index 00000000000..40a399284fe --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-aa7f-weak_1.json @@ -0,0 +1,44 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:19 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1043", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak?alt=json" + }, + "body": { + "kind": "compute#sslPolicy", + "id": "2849015264028168069", + "creationTimestamp": "2026-09-30T08:57:30.473-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "name": "c7n-default-aa7f-weak", + "profile": "COMPATIBLE", + "minTlsVersion": "TLS_1_0", + "enabledFeatures": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ], + "fingerprint": "UCiJtMU0f-Q=" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json new file mode 100644 index 00000000000..18bf6cc600b --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-https-ssl-shared/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetHttpsProxies_1.json @@ -0,0 +1,84 @@ +{ + "headers": { + "etag": "vprPdDSXZPNz6vHoz2Qi547GiZ0=/KHifhhKrXlDVHL0XwG2W82kdtbA=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:58:19 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "3475", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies?alt=json" + }, + "body": { + "kind": "compute#targetHttpsProxyList", + "id": "projects/cloud-custodian/global/targetHttpsProxies", + "items": [ + { + "kind": "compute#targetHttpsProxy", + "id": "3657098629032742754", + "creationTimestamp": "2026-09-30T08:58:05.527-07:00", + "name": "c7n-default-aa7f-https-good", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "fingerprint": "eWmtHCE-9FE=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2603121230916652898", + "creationTimestamp": "2026-09-30T08:58:05.953-07:00", + "name": "c7n-default-aa7f-https-none", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "fingerprint": "tUieqhM7-us=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "203287616958795618", + "creationTimestamp": "2026-09-30T08:58:05.498-07:00", + "name": "c7n-default-aa7f-https-weak", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + }, + { + "kind": "compute#targetHttpsProxy", + "id": "2427009597342519138", + "creationTimestamp": "2026-09-30T08:58:05.407-07:00", + "name": "c7n-default-aa7f-https-weak2", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "urlMap": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "quicOverride": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "fingerprint": "1qrgmeFF278=", + "tlsEarlyData": "DISABLED" + } + ], + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-3e3a-weak_1.json b/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-3e3a-weak_1.json new file mode 100644 index 00000000000..47f80c7da9b --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-sslPolicies-c7n-default-3e3a-weak_1.json @@ -0,0 +1,44 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:59:47 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1043", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak?alt=json" + }, + "body": { + "kind": "compute#sslPolicy", + "id": "5845612821165936425", + "creationTimestamp": "2026-09-30T08:59:02.633-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak", + "name": "c7n-default-3e3a-weak", + "profile": "COMPATIBLE", + "minTlsVersion": "TLS_1_0", + "enabledFeatures": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ], + "fingerprint": "UCiJtMU0f-Q=" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetSslProxies_1.json b/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetSslProxies_1.json new file mode 100644 index 00000000000..98ebf450528 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/lb-sslproxy-ssl-weak/get-compute.googleapis.com-compute-v1-projects-cloud-custodian-global-targetSslProxies_1.json @@ -0,0 +1,38 @@ +{ + "headers": { + "etag": "P45GEQcVzhDmDx3GokUK8Dzk1Kg=/B8Bz4HC4JqgY7PEe5iG9sSU1DFo=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 30 Sep 2026 15:59:47 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "995", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/global/targetSslProxies?alt=json" + }, + "body": { + "kind": "compute#targetSslProxyList", + "id": "projects/cloud-custodian/global/targetSslProxies", + "items": [ + { + "kind": "compute#targetSslProxy", + "id": "4588591286513078024", + "creationTimestamp": "2026-09-30T08:59:35.339-07:00", + "name": "c7n-default-3e3a-ssl-weak", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetSslProxies/c7n-default-3e3a-ssl-weak", + "service": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/backendServices/c7n-default-3e3a-bs", + "sslCertificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-3e3a-cert" + ], + "proxyHeader": "NONE", + "sslPolicy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak" + } + ], + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetSslProxies" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/main.tf b/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/main.tf new file mode 100644 index 00000000000..d89d66746ae --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/main.tf @@ -0,0 +1,83 @@ +resource "random_id" "suffix" { + byte_length = 2 +} + +locals { + prefix = "c7n-${terraform.workspace}-${random_id.suffix.hex}" +} + +resource "tls_private_key" "default" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "default" { + private_key_pem = tls_private_key.default.private_key_pem + validity_period_hours = 24 + allowed_uses = ["key_encipherment", "digital_signature", "server_auth"] + + subject { + common_name = "example.com" + } +} + +resource "google_compute_ssl_certificate" "default" { + name = "${local.prefix}-cert" + private_key = tls_private_key.default.private_key_pem + certificate = tls_self_signed_cert.default.cert_pem +} + +resource "google_compute_ssl_policy" "weak" { + name = "${local.prefix}-weak" + profile = "COMPATIBLE" +} + +resource "google_compute_ssl_policy" "good" { + name = "${local.prefix}-good" + profile = "MODERN" + min_tls_version = "TLS_1_2" +} + +resource "google_storage_bucket" "default" { + name = "${local.prefix}-lb-bucket" + location = "US" + force_destroy = true + uniform_bucket_level_access = true +} + +resource "google_compute_backend_bucket" "default" { + name = "${local.prefix}-bb" + bucket_name = google_storage_bucket.default.name +} + +resource "google_compute_url_map" "default" { + name = "${local.prefix}-urlmap" + default_service = google_compute_backend_bucket.default.id +} + +resource "google_compute_target_https_proxy" "weak" { + name = "${local.prefix}-https-weak" + url_map = google_compute_url_map.default.id + ssl_certificates = [google_compute_ssl_certificate.default.id] + ssl_policy = google_compute_ssl_policy.weak.id +} + +resource "google_compute_target_https_proxy" "weak2" { + name = "${local.prefix}-https-weak2" + url_map = google_compute_url_map.default.id + ssl_certificates = [google_compute_ssl_certificate.default.id] + ssl_policy = google_compute_ssl_policy.weak.id +} + +resource "google_compute_target_https_proxy" "good" { + name = "${local.prefix}-https-good" + url_map = google_compute_url_map.default.id + ssl_certificates = [google_compute_ssl_certificate.default.id] + ssl_policy = google_compute_ssl_policy.good.id +} + +resource "google_compute_target_https_proxy" "none" { + name = "${local.prefix}-https-none" + url_map = google_compute_url_map.default.id + ssl_certificates = [google_compute_ssl_certificate.default.id] +} diff --git a/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/tf_resources.json b/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/tf_resources.json new file mode 100644 index 00000000000..b9c887d85aa --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/lb_https_proxy_ssl_policy/tf_resources.json @@ -0,0 +1,345 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_compute_backend_bucket": { + "default": { + "bucket_name": "c7n-default-aa7f-lb-bucket", + "cdn_policy": [], + "compression_mode": "", + "creation_timestamp": "2026-09-30T08:57:32.202-07:00", + "custom_response_headers": null, + "deletion_policy": "DELETE", + "description": "", + "edge_security_policy": "", + "enable_cdn": false, + "id": "projects/cloud-custodian/global/backendBuckets/c7n-default-aa7f-bb", + "load_balancing_scheme": "", + "name": "c7n-default-aa7f-bb", + "params": [], + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/backendBuckets/c7n-default-aa7f-bb", + "timeouts": null + } + }, + "google_compute_ssl_certificate": { + "default": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIC7DCCAdSgAwIBAgIRAJUzlgwbH7dS5G7Gwuxe4D0wDQYJKoZIhvcNAQELBQAw\nFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjYwOTMwMTU1NzI5WhcNMjYxMDAx\nMTU1NzI5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAK82FB6J+TNrotsGBerJwv0lqSZNFe9BjzClAjINS6Oj\n6l3zeKuvi7n1frEBRuzTTvdC2GdU/jZJ5jEixmwG5L7eCPD/kYIAOHsu5RuW4hv2\nFR908zBOrhB/sW349ZznaOZpcyGI25aBh7WRMsU4yWCk3CL9TzgUKXsBlFMWLcIx\nM1AUdHqzZ47BBv8IVh+plOwXcuM2ipJttriBJW2HBzNcsHGEx60EY+WB+fvtClQR\nuB+Y9T91tlOQUl1eJCTGYnBxib355/VySGgZuECDjv9YwlvWGkfa+Og3GwUNdhOY\n4FV5fWPLmizdyc+tmZB9Tkr+Oy6QQTZj+xlFgM8ojs0CAwEAAaM1MDMwDgYDVR0P\nAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJ\nKoZIhvcNAQELBQADggEBAG302lsippQuu7qGRbeJ90X5EiZFmWXQeZE2IbXwCjfA\nvXJKMQqaMHavkSV5D+AsfoQ9DB2x91v4wqMLHcG0LBRrmb2KinbUwOKgQQlQ1HAp\nHsyBp5X994pNnZeEgKyTDZVzgpKZRDtaT/N/b1eCA4xyNm09pWlxUpsEioyBc2XI\nE3d6d7X/gmnxwd1KiKudG/qbeGGwrj7djjlX9KgX92nO0KKaFQdry1JcieV/y4b+\nJ7XUucO9KucvN+JAoZ7xICPD2Sjqx/1b2SrPo9s7c1YOmIwQ48fopgbWG0vjAfkf\nf75KJNSz2v1zUiCuP8dCPwKvZ3B93MCRdoqiLFNCtTc=\n-----END CERTIFICATE-----\n", + "certificate_id": 3523437958291033989, + "creation_timestamp": "2026-09-30T08:57:30.542-07:00", + "deletion_policy": "DELETE", + "description": "", + "expire_time": "2026-10-01T08:57:29.000-07:00", + "id": "projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert", + "name": "c7n-default-aa7f-cert", + "name_prefix": null, + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArzYUHon5M2ui2wYF6snC/SWpJk0V70GPMKUCMg1Lo6PqXfN4\nq6+LufV+sQFG7NNO90LYZ1T+NknmMSLGbAbkvt4I8P+RggA4ey7lG5biG/YVH3Tz\nME6uEH+xbfj1nOdo5mlzIYjbloGHtZEyxTjJYKTcIv1POBQpewGUUxYtwjEzUBR0\nerNnjsEG/whWH6mU7Bdy4zaKkm22uIElbYcHM1ywcYTHrQRj5YH5++0KVBG4H5j1\nP3W2U5BSXV4kJMZicHGJvfnn9XJIaBm4QIOO/1jCW9YaR9r46DcbBQ12E5jgVXl9\nY8uaLN3Jz62ZkH1OSv47LpBBNmP7GUWAzyiOzQIDAQABAoIBAChNzM//lhMFggdW\n+dp6AgAgfyn/1nlcf9dFRd2EoG8qoBlvjcUUkxQ6TCPkcs+8hfAr8MLIUtfjJPAN\nm04zQcBELBNf/rVEXrm7Xll72lRrRY2gZeapWrIR5ZsyBzHKK5/BBwpiRHz5G+Ig\n3ehvOSehlL/5vBTe0nHouZlpQyplrHlLEShLN1CLqa1Sgi1eVYgwL5LSNf3BD00P\nrbzQfDCxtsgO2jAcdsEoy3tJLhUsZgTwETYIIpht7DTVYAr8v4xJr86d2zCSSPYx\nUa0lEBNhZAumeCx/zlRQmMEz43tUD8T80N0jQJTlbXh7mOdzXxnWCXzTnIi2vtuM\nJgE2PEECgYEAzb1eJJTpl2X2trR9Bw33C+ilDPClPhs03dfBe93h1BQbQlQFxwN+\nURuMVg5N2d4IJilnGUwL2QVw/KUrfGK2yKs95BC49f939zXPxI9NIMV8j+fIBNEK\nJ2mzjJA2q6UlXQQFB/FgRgo3NzRZFQ3VRkV1xS4+fSBulCahWSTrwqECgYEA2gOC\nUQd7Lgs+TvMuZ54W2EXW8quqj7hnTF1u9m9iwJBcRx8syt3MOZjqwMT3Uu1Jw6jy\nIn0xbOFMUNMYPj9rbzKmabgx/xsDo6hSBSzMNZ7DUiXmGJsuZ5SXODIB0WYHHs4/\njm+BSBfZuOjMFYmWqKBQdb+0SxBTGDwpRQchCK0CgYEAsirVxjQ2/edM+fffdiZ/\n1I5JsKUNVHVXGO5pHg2OLYyj9pKw1iGOWErVAwksHgoLYJDmOaADepMdv4yB1hve\nUnUJCeH5av8rMqglHwFCN6kOL8j3DETC2FLPLGEPFyHS83Qxryk2yDC8lE/+/R6X\nnV01meZsMwvM9vtlKr+wc0ECgYBf1PsLs0YqTzbsbWkQkyVnnAuIUH2A/4Yow0lr\nejrmeo6VlNaTtjt3dgh7BBnTkgrCvxjwoCeBvOdW//PAKRCIqLMN9bHEeZN39R1Y\nuQBq8YbaLV6folcboO13/kNeUIMAv5ige9NQZWuHJI3RSdG3/gBgQgx2ko50ML3T\nYfbbYQKBgCYPdZqE3C16bEmL4eIaWE2iR+WDFsLhJV/FitrdeXCeR+WxJLQ7dtXq\n67BwQ+TKM3ZPIJHRo90mBEdFWouAXHJpQj9yW828UZUKvI9SKaC0Pam0Lp2qh+bS\nsYHy3LpNDj4RrxjXvKOchlQDebXQX28sJN/apmlV2UwYgzsz1JvJ\n-----END RSA PRIVATE KEY-----\n", + "private_key_wo": null, + "private_key_wo_version": "", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert", + "timeouts": null + } + }, + "google_compute_ssl_policy": { + "good": { + "creation_timestamp": "2026-09-30T08:57:30.457-07:00", + "custom_features": null, + "deletion_policy": "DELETE", + "description": "", + "enabled_features": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + ], + "fingerprint": "4JopUddr0yY=", + "id": "projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "min_tls_version": "TLS_1_2", + "name": "c7n-default-aa7f-good", + "post_quantum_key_exchange": "", + "profile": "MODERN", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "timeouts": null + }, + "weak": { + "creation_timestamp": "2026-09-30T08:57:30.473-07:00", + "custom_features": null, + "deletion_policy": "DELETE", + "description": "", + "enabled_features": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ], + "fingerprint": "UCiJtMU0f-Q=", + "id": "projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "min_tls_version": "TLS_1_0", + "name": "c7n-default-aa7f-weak", + "post_quantum_key_exchange": "", + "profile": "COMPATIBLE", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "timeouts": null + } + }, + "google_compute_target_https_proxy": { + "good": { + "certificate_manager_certificates": null, + "certificate_map": "", + "creation_timestamp": "2026-09-30T08:58:05.527-07:00", + "deletion_policy": "DELETE", + "description": "", + "fingerprint": "eWmtHCE-9FE=", + "http_keep_alive_timeout_sec": 0, + "id": "projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "name": "c7n-default-aa7f-https-good", + "project": "stkl-kody-mullins", + "proxy_bind": false, + "proxy_id": 3657098629032742754, + "quic_override": "NONE", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-good", + "server_tls_policy": "", + "ssl_certificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "ssl_policy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-good", + "timeouts": null, + "tls_early_data": "DISABLED", + "url_map": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap" + }, + "none": { + "certificate_manager_certificates": null, + "certificate_map": "", + "creation_timestamp": "2026-09-30T08:58:05.953-07:00", + "deletion_policy": "DELETE", + "description": "", + "fingerprint": "tUieqhM7-us=", + "http_keep_alive_timeout_sec": 0, + "id": "projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "name": "c7n-default-aa7f-https-none", + "project": "stkl-kody-mullins", + "proxy_bind": false, + "proxy_id": 2603121230916652898, + "quic_override": "NONE", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-none", + "server_tls_policy": "", + "ssl_certificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "ssl_policy": "", + "timeouts": null, + "tls_early_data": "DISABLED", + "url_map": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap" + }, + "weak": { + "certificate_manager_certificates": null, + "certificate_map": "", + "creation_timestamp": "2026-09-30T08:58:05.498-07:00", + "deletion_policy": "DELETE", + "description": "", + "fingerprint": "1qrgmeFF278=", + "http_keep_alive_timeout_sec": 0, + "id": "projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "name": "c7n-default-aa7f-https-weak", + "project": "stkl-kody-mullins", + "proxy_bind": false, + "proxy_id": 203287616958795618, + "quic_override": "NONE", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak", + "server_tls_policy": "", + "ssl_certificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "ssl_policy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "timeouts": null, + "tls_early_data": "DISABLED", + "url_map": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap" + }, + "weak2": { + "certificate_manager_certificates": null, + "certificate_map": "", + "creation_timestamp": "2026-09-30T08:58:05.407-07:00", + "deletion_policy": "DELETE", + "description": "", + "fingerprint": "1qrgmeFF278=", + "http_keep_alive_timeout_sec": 0, + "id": "projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "name": "c7n-default-aa7f-https-weak2", + "project": "stkl-kody-mullins", + "proxy_bind": false, + "proxy_id": 2427009597342519138, + "quic_override": "NONE", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetHttpsProxies/c7n-default-aa7f-https-weak2", + "server_tls_policy": "", + "ssl_certificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-aa7f-cert" + ], + "ssl_policy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-aa7f-weak", + "timeouts": null, + "tls_early_data": "DISABLED", + "url_map": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap" + } + }, + "google_compute_url_map": { + "default": { + "creation_timestamp": "2026-09-30T08:57:53.844-07:00", + "default_custom_error_response_policy": [], + "default_route_action": [], + "default_service": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/backendBuckets/c7n-default-aa7f-bb", + "default_url_redirect": [], + "deletion_policy": "DELETE", + "description": "", + "fingerprint": "HWcRbmRdKg8=", + "header_action": [], + "host_rule": [], + "id": "projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "map_id": 7614190026201534318, + "name": "c7n-default-aa7f-urlmap", + "path_matcher": [], + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/urlMaps/c7n-default-aa7f-urlmap", + "test": [], + "timeouts": null + } + }, + "google_storage_bucket": { + "default": { + "autoclass": [], + "cors": [], + "custom_placement_config": [], + "default_event_based_hold": false, + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_object_retention": false, + "encryption": [], + "force_destroy": true, + "hierarchical_namespace": [ + { + "enabled": false + } + ], + "id": "c7n-default-aa7f-lb-bucket", + "ip_filter": [], + "labels": null, + "lifecycle_rule": [], + "location": "US", + "logging": [], + "name": "c7n-default-aa7f-lb-bucket", + "project": "stkl-kody-mullins", + "project_number": 82151825516, + "public_access_prevention": "inherited", + "requester_pays": false, + "retention_policy": [], + "rpo": "DEFAULT", + "self_link": "https://www.googleapis.com/storage/v1/b/c7n-default-aa7f-lb-bucket", + "soft_delete_policy": [ + { + "effective_time": "2026-09-30T15:57:31.241Z", + "retention_duration_seconds": 604800 + } + ], + "storage_class": "STANDARD", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "time_created": "2026-09-30T15:57:31.241Z", + "timeouts": null, + "uniform_bucket_level_access": true, + "updated": "2026-09-30T15:57:31.241Z", + "url": "gs://c7n-default-aa7f-lb-bucket", + "versioning": [], + "website": [] + } + }, + "random_id": { + "suffix": { + "b64_std": "qn8=", + "b64_url": "qn8", + "byte_length": 2, + "dec": "43647", + "hex": "aa7f", + "id": "qn8", + "keepers": null, + "prefix": null + } + }, + "tls_private_key": { + "default": { + "algorithm": "RSA", + "ecdsa_curve": "P224", + "id": "ade6aed86d7300b3945286c48c11cd34badaca5d", + "private_key_openssh": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\nc2gtcnNhAAAAAwEAAQAAAQEArzYUHon5M2ui2wYF6snC/SWpJk0V70GPMKUCMg1L\no6PqXfN4q6+LufV+sQFG7NNO90LYZ1T+NknmMSLGbAbkvt4I8P+RggA4ey7lG5bi\nG/YVH3TzME6uEH+xbfj1nOdo5mlzIYjbloGHtZEyxTjJYKTcIv1POBQpewGUUxYt\nwjEzUBR0erNnjsEG/whWH6mU7Bdy4zaKkm22uIElbYcHM1ywcYTHrQRj5YH5++0K\nVBG4H5j1P3W2U5BSXV4kJMZicHGJvfnn9XJIaBm4QIOO/1jCW9YaR9r46DcbBQ12\nE5jgVXl9Y8uaLN3Jz62ZkH1OSv47LpBBNmP7GUWAzyiOzQAAA7jH0UIlx9FCJQAA\nAAdzc2gtcnNhAAABAQCvNhQeifkza6LbBgXqycL9JakmTRXvQY8wpQIyDUujo+pd\n83irr4u59X6xAUbs0073QthnVP42SeYxIsZsBuS+3gjw/5GCADh7LuUbluIb9hUf\ndPMwTq4Qf7Ft+PWc52jmaXMhiNuWgYe1kTLFOMlgpNwi/U84FCl7AZRTFi3CMTNQ\nFHR6s2eOwQb/CFYfqZTsF3LjNoqSbba4gSVthwczXLBxhMetBGPlgfn77QpUEbgf\nmPU/dbZTkFJdXiQkxmJwcYm9+ef1ckhoGbhAg47/WMJb1hpH2vjoNxsFDXYTmOBV\neX1jy5os3cnPrZmQfU5K/jsukEE2Y/sZRYDPKI7NAAAAAwEAAQAAAQAoTczP/5YT\nBYIHVvnaegIAIH8p/9Z5XH/XRUXdhKBvKqAZb43FFJMUOkwj5HLPvIXwK/DCyFLX\n4yTwDZtOM0HARCwTX/61RF65u15Ze9pUa0WNoGXmqVqyEeWbMgcxyiufwQcKYkR8\n+RviIN3obzknoZS/+bwU3tJx6LmZaUMqZax5SxEoSzdQi6mtUoItXlWIMC+S0jX9\nwQ9ND6280HwwsbbIDtowHHbBKMt7SS4VLGYE8BE2CCKYbew01WAK/L+MSa/Ondsw\nkkj2MVGtJRATYWQLpngsf85UUJjBM+N7VA/E/NDdI0CU5W14e5jnc18Z1gl805yI\ntr7bjCYBNjxBAAAAgCYPdZqE3C16bEmL4eIaWE2iR+WDFsLhJV/FitrdeXCeR+Wx\nJLQ7dtXq67BwQ+TKM3ZPIJHRo90mBEdFWouAXHJpQj9yW828UZUKvI9SKaC0Pam0\nLp2qh+bSsYHy3LpNDj4RrxjXvKOchlQDebXQX28sJN/apmlV2UwYgzsz1JvJAAAA\ngQDNvV4klOmXZfa2tH0HDfcL6KUM8KU+GzTd18F73eHUFBtCVAXHA35RG4xWDk3Z\n3ggmKWcZTAvZBXD8pSt8YrbIqz3kELj1/3f3Nc/Ej00gxXyP58gE0QonabOMkDar\npSVdBAUH8WBGCjc3NFkVDdVGRXXFLj59IG6UJqFZJOvCoQAAAIEA2gOCUQd7Lgs+\nTvMuZ54W2EXW8quqj7hnTF1u9m9iwJBcRx8syt3MOZjqwMT3Uu1Jw6jyIn0xbOFM\nUNMYPj9rbzKmabgx/xsDo6hSBSzMNZ7DUiXmGJsuZ5SXODIB0WYHHs4/jm+BSBfZ\nuOjMFYmWqKBQdb+0SxBTGDwpRQchCK0AAAAAAQID\n-----END OPENSSH PRIVATE KEY-----\n", + "private_key_pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArzYUHon5M2ui2wYF6snC/SWpJk0V70GPMKUCMg1Lo6PqXfN4\nq6+LufV+sQFG7NNO90LYZ1T+NknmMSLGbAbkvt4I8P+RggA4ey7lG5biG/YVH3Tz\nME6uEH+xbfj1nOdo5mlzIYjbloGHtZEyxTjJYKTcIv1POBQpewGUUxYtwjEzUBR0\nerNnjsEG/whWH6mU7Bdy4zaKkm22uIElbYcHM1ywcYTHrQRj5YH5++0KVBG4H5j1\nP3W2U5BSXV4kJMZicHGJvfnn9XJIaBm4QIOO/1jCW9YaR9r46DcbBQ12E5jgVXl9\nY8uaLN3Jz62ZkH1OSv47LpBBNmP7GUWAzyiOzQIDAQABAoIBAChNzM//lhMFggdW\n+dp6AgAgfyn/1nlcf9dFRd2EoG8qoBlvjcUUkxQ6TCPkcs+8hfAr8MLIUtfjJPAN\nm04zQcBELBNf/rVEXrm7Xll72lRrRY2gZeapWrIR5ZsyBzHKK5/BBwpiRHz5G+Ig\n3ehvOSehlL/5vBTe0nHouZlpQyplrHlLEShLN1CLqa1Sgi1eVYgwL5LSNf3BD00P\nrbzQfDCxtsgO2jAcdsEoy3tJLhUsZgTwETYIIpht7DTVYAr8v4xJr86d2zCSSPYx\nUa0lEBNhZAumeCx/zlRQmMEz43tUD8T80N0jQJTlbXh7mOdzXxnWCXzTnIi2vtuM\nJgE2PEECgYEAzb1eJJTpl2X2trR9Bw33C+ilDPClPhs03dfBe93h1BQbQlQFxwN+\nURuMVg5N2d4IJilnGUwL2QVw/KUrfGK2yKs95BC49f939zXPxI9NIMV8j+fIBNEK\nJ2mzjJA2q6UlXQQFB/FgRgo3NzRZFQ3VRkV1xS4+fSBulCahWSTrwqECgYEA2gOC\nUQd7Lgs+TvMuZ54W2EXW8quqj7hnTF1u9m9iwJBcRx8syt3MOZjqwMT3Uu1Jw6jy\nIn0xbOFMUNMYPj9rbzKmabgx/xsDo6hSBSzMNZ7DUiXmGJsuZ5SXODIB0WYHHs4/\njm+BSBfZuOjMFYmWqKBQdb+0SxBTGDwpRQchCK0CgYEAsirVxjQ2/edM+fffdiZ/\n1I5JsKUNVHVXGO5pHg2OLYyj9pKw1iGOWErVAwksHgoLYJDmOaADepMdv4yB1hve\nUnUJCeH5av8rMqglHwFCN6kOL8j3DETC2FLPLGEPFyHS83Qxryk2yDC8lE/+/R6X\nnV01meZsMwvM9vtlKr+wc0ECgYBf1PsLs0YqTzbsbWkQkyVnnAuIUH2A/4Yow0lr\nejrmeo6VlNaTtjt3dgh7BBnTkgrCvxjwoCeBvOdW//PAKRCIqLMN9bHEeZN39R1Y\nuQBq8YbaLV6folcboO13/kNeUIMAv5ige9NQZWuHJI3RSdG3/gBgQgx2ko50ML3T\nYfbbYQKBgCYPdZqE3C16bEmL4eIaWE2iR+WDFsLhJV/FitrdeXCeR+WxJLQ7dtXq\n67BwQ+TKM3ZPIJHRo90mBEdFWouAXHJpQj9yW828UZUKvI9SKaC0Pam0Lp2qh+bS\nsYHy3LpNDj4RrxjXvKOchlQDebXQX28sJN/apmlV2UwYgzsz1JvJ\n-----END RSA PRIVATE KEY-----\n", + "private_key_pem_pkcs8": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvNhQeifkza6Lb\nBgXqycL9JakmTRXvQY8wpQIyDUujo+pd83irr4u59X6xAUbs0073QthnVP42SeYx\nIsZsBuS+3gjw/5GCADh7LuUbluIb9hUfdPMwTq4Qf7Ft+PWc52jmaXMhiNuWgYe1\nkTLFOMlgpNwi/U84FCl7AZRTFi3CMTNQFHR6s2eOwQb/CFYfqZTsF3LjNoqSbba4\ngSVthwczXLBxhMetBGPlgfn77QpUEbgfmPU/dbZTkFJdXiQkxmJwcYm9+ef1ckho\nGbhAg47/WMJb1hpH2vjoNxsFDXYTmOBVeX1jy5os3cnPrZmQfU5K/jsukEE2Y/sZ\nRYDPKI7NAgMBAAECggEAKE3Mz/+WEwWCB1b52noCACB/Kf/WeVx/10VF3YSgbyqg\nGW+NxRSTFDpMI+Ryz7yF8CvwwshS1+Mk8A2bTjNBwEQsE1/+tUReubteWXvaVGtF\njaBl5qlashHlmzIHMcorn8EHCmJEfPkb4iDd6G85J6GUv/m8FN7Scei5mWlDKmWs\neUsRKEs3UIuprVKCLV5ViDAvktI1/cEPTQ+tvNB8MLG2yA7aMBx2wSjLe0kuFSxm\nBPARNggimG3sNNVgCvy/jEmvzp3bMJJI9jFRrSUQE2FkC6Z4LH/OVFCYwTPje1QP\nxPzQ3SNAlOVteHuY53NfGdYJfNOciLa+24wmATY8QQKBgQDNvV4klOmXZfa2tH0H\nDfcL6KUM8KU+GzTd18F73eHUFBtCVAXHA35RG4xWDk3Z3ggmKWcZTAvZBXD8pSt8\nYrbIqz3kELj1/3f3Nc/Ej00gxXyP58gE0QonabOMkDarpSVdBAUH8WBGCjc3NFkV\nDdVGRXXFLj59IG6UJqFZJOvCoQKBgQDaA4JRB3suCz5O8y5nnhbYRdbyq6qPuGdM\nXW72b2LAkFxHHyzK3cw5mOrAxPdS7UnDqPIifTFs4UxQ0xg+P2tvMqZpuDH/GwOj\nqFIFLMw1nsNSJeYYmy5nlJc4MgHRZgcezj+Ob4FIF9m46MwViZaooFB1v7RLEFMY\nPClFByEIrQKBgQCyKtXGNDb950z59992Jn/UjkmwpQ1UdVcY7mkeDY4tjKP2krDW\nIY5YStUDCSweCgtgkOY5oAN6kx2/jIHWG95SdQkJ4flq/ysyqCUfAUI3qQ4vyPcM\nRMLYUs8sYQ8XIdLzdDGvKTbIMLyUT/79HpedXTWZ5mwzC8z2+2Uqv7BzQQKBgF/U\n+wuzRipPNuxtaRCTJWecC4hQfYD/hijDSWt6OuZ6jpWU1pO2O3d2CHsEGdOSCsK/\nGPCgJ4G851b/88ApEIiosw31scR5k3f1HVi5AGrxhtotXp+iVxug7Xf+Q15QgwC/\nmKB701Bla4ckjdFJ0bf+AGBCDHaSjnQwvdNh9tthAoGAJg91moTcLXpsSYvh4hpY\nTaJH5YMWwuElX8WK2t15cJ5H5bEktDt21errsHBD5Mozdk8gkdGj3SYER0Vai4Bc\ncmlCP3JbzbxRlQq8j1IpoLQ9qbQunaqH5tKxgfLcuk0OPhGvGNe8o5yGVAN5tdBf\nbywk39qmaVXZTBiDOzPUm8k=\n-----END PRIVATE KEY-----\n", + "public_key_fingerprint_md5": "db:8b:45:79:f2:83:af:ad:e0:1b:f5:c5:6c:de:fe:a3", + "public_key_fingerprint_sha256": "SHA256:Q2HTXhxNDQMAsa0A+WgCZwMCPEsWbvn1laT+x8Zd+pQ", + "public_key_openssh": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvNhQeifkza6LbBgXqycL9JakmTRXvQY8wpQIyDUujo+pd83irr4u59X6xAUbs0073QthnVP42SeYxIsZsBuS+3gjw/5GCADh7LuUbluIb9hUfdPMwTq4Qf7Ft+PWc52jmaXMhiNuWgYe1kTLFOMlgpNwi/U84FCl7AZRTFi3CMTNQFHR6s2eOwQb/CFYfqZTsF3LjNoqSbba4gSVthwczXLBxhMetBGPlgfn77QpUEbgfmPU/dbZTkFJdXiQkxmJwcYm9+ef1ckhoGbhAg47/WMJb1hpH2vjoNxsFDXYTmOBVeX1jy5os3cnPrZmQfU5K/jsukEE2Y/sZRYDPKI7N\n", + "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArzYUHon5M2ui2wYF6snC\n/SWpJk0V70GPMKUCMg1Lo6PqXfN4q6+LufV+sQFG7NNO90LYZ1T+NknmMSLGbAbk\nvt4I8P+RggA4ey7lG5biG/YVH3TzME6uEH+xbfj1nOdo5mlzIYjbloGHtZEyxTjJ\nYKTcIv1POBQpewGUUxYtwjEzUBR0erNnjsEG/whWH6mU7Bdy4zaKkm22uIElbYcH\nM1ywcYTHrQRj5YH5++0KVBG4H5j1P3W2U5BSXV4kJMZicHGJvfnn9XJIaBm4QIOO\n/1jCW9YaR9r46DcbBQ12E5jgVXl9Y8uaLN3Jz62ZkH1OSv47LpBBNmP7GUWAzyiO\nzQIDAQAB\n-----END PUBLIC KEY-----\n", + "rsa_bits": 2048 + } + }, + "tls_self_signed_cert": { + "default": { + "allowed_uses": [ + "key_encipherment", + "digital_signature", + "server_auth" + ], + "cert_pem": "-----BEGIN CERTIFICATE-----\nMIIC7DCCAdSgAwIBAgIRAJUzlgwbH7dS5G7Gwuxe4D0wDQYJKoZIhvcNAQELBQAw\nFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjYwOTMwMTU1NzI5WhcNMjYxMDAx\nMTU1NzI5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAK82FB6J+TNrotsGBerJwv0lqSZNFe9BjzClAjINS6Oj\n6l3zeKuvi7n1frEBRuzTTvdC2GdU/jZJ5jEixmwG5L7eCPD/kYIAOHsu5RuW4hv2\nFR908zBOrhB/sW349ZznaOZpcyGI25aBh7WRMsU4yWCk3CL9TzgUKXsBlFMWLcIx\nM1AUdHqzZ47BBv8IVh+plOwXcuM2ipJttriBJW2HBzNcsHGEx60EY+WB+fvtClQR\nuB+Y9T91tlOQUl1eJCTGYnBxib355/VySGgZuECDjv9YwlvWGkfa+Og3GwUNdhOY\n4FV5fWPLmizdyc+tmZB9Tkr+Oy6QQTZj+xlFgM8ojs0CAwEAAaM1MDMwDgYDVR0P\nAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJ\nKoZIhvcNAQELBQADggEBAG302lsippQuu7qGRbeJ90X5EiZFmWXQeZE2IbXwCjfA\nvXJKMQqaMHavkSV5D+AsfoQ9DB2x91v4wqMLHcG0LBRrmb2KinbUwOKgQQlQ1HAp\nHsyBp5X994pNnZeEgKyTDZVzgpKZRDtaT/N/b1eCA4xyNm09pWlxUpsEioyBc2XI\nE3d6d7X/gmnxwd1KiKudG/qbeGGwrj7djjlX9KgX92nO0KKaFQdry1JcieV/y4b+\nJ7XUucO9KucvN+JAoZ7xICPD2Sjqx/1b2SrPo9s7c1YOmIwQ48fopgbWG0vjAfkf\nf75KJNSz2v1zUiCuP8dCPwKvZ3B93MCRdoqiLFNCtTc=\n-----END CERTIFICATE-----\n", + "dns_names": null, + "early_renewal_hours": 0, + "id": "198322821832310676881202012282601529405", + "ip_addresses": null, + "is_ca_certificate": false, + "key_algorithm": "RSA", + "max_path_length": null, + "private_key_pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArzYUHon5M2ui2wYF6snC/SWpJk0V70GPMKUCMg1Lo6PqXfN4\nq6+LufV+sQFG7NNO90LYZ1T+NknmMSLGbAbkvt4I8P+RggA4ey7lG5biG/YVH3Tz\nME6uEH+xbfj1nOdo5mlzIYjbloGHtZEyxTjJYKTcIv1POBQpewGUUxYtwjEzUBR0\nerNnjsEG/whWH6mU7Bdy4zaKkm22uIElbYcHM1ywcYTHrQRj5YH5++0KVBG4H5j1\nP3W2U5BSXV4kJMZicHGJvfnn9XJIaBm4QIOO/1jCW9YaR9r46DcbBQ12E5jgVXl9\nY8uaLN3Jz62ZkH1OSv47LpBBNmP7GUWAzyiOzQIDAQABAoIBAChNzM//lhMFggdW\n+dp6AgAgfyn/1nlcf9dFRd2EoG8qoBlvjcUUkxQ6TCPkcs+8hfAr8MLIUtfjJPAN\nm04zQcBELBNf/rVEXrm7Xll72lRrRY2gZeapWrIR5ZsyBzHKK5/BBwpiRHz5G+Ig\n3ehvOSehlL/5vBTe0nHouZlpQyplrHlLEShLN1CLqa1Sgi1eVYgwL5LSNf3BD00P\nrbzQfDCxtsgO2jAcdsEoy3tJLhUsZgTwETYIIpht7DTVYAr8v4xJr86d2zCSSPYx\nUa0lEBNhZAumeCx/zlRQmMEz43tUD8T80N0jQJTlbXh7mOdzXxnWCXzTnIi2vtuM\nJgE2PEECgYEAzb1eJJTpl2X2trR9Bw33C+ilDPClPhs03dfBe93h1BQbQlQFxwN+\nURuMVg5N2d4IJilnGUwL2QVw/KUrfGK2yKs95BC49f939zXPxI9NIMV8j+fIBNEK\nJ2mzjJA2q6UlXQQFB/FgRgo3NzRZFQ3VRkV1xS4+fSBulCahWSTrwqECgYEA2gOC\nUQd7Lgs+TvMuZ54W2EXW8quqj7hnTF1u9m9iwJBcRx8syt3MOZjqwMT3Uu1Jw6jy\nIn0xbOFMUNMYPj9rbzKmabgx/xsDo6hSBSzMNZ7DUiXmGJsuZ5SXODIB0WYHHs4/\njm+BSBfZuOjMFYmWqKBQdb+0SxBTGDwpRQchCK0CgYEAsirVxjQ2/edM+fffdiZ/\n1I5JsKUNVHVXGO5pHg2OLYyj9pKw1iGOWErVAwksHgoLYJDmOaADepMdv4yB1hve\nUnUJCeH5av8rMqglHwFCN6kOL8j3DETC2FLPLGEPFyHS83Qxryk2yDC8lE/+/R6X\nnV01meZsMwvM9vtlKr+wc0ECgYBf1PsLs0YqTzbsbWkQkyVnnAuIUH2A/4Yow0lr\nejrmeo6VlNaTtjt3dgh7BBnTkgrCvxjwoCeBvOdW//PAKRCIqLMN9bHEeZN39R1Y\nuQBq8YbaLV6folcboO13/kNeUIMAv5ige9NQZWuHJI3RSdG3/gBgQgx2ko50ML3T\nYfbbYQKBgCYPdZqE3C16bEmL4eIaWE2iR+WDFsLhJV/FitrdeXCeR+WxJLQ7dtXq\n67BwQ+TKM3ZPIJHRo90mBEdFWouAXHJpQj9yW828UZUKvI9SKaC0Pam0Lp2qh+bS\nsYHy3LpNDj4RrxjXvKOchlQDebXQX28sJN/apmlV2UwYgzsz1JvJ\n-----END RSA PRIVATE KEY-----\n", + "private_key_pem_wo": null, + "private_key_pem_wo_version": null, + "ready_for_renewal": false, + "set_authority_key_id": false, + "set_subject_key_id": false, + "subject": [ + { + "common_name": "example.com", + "country": null, + "email_address": null, + "locality": null, + "organization": null, + "organizational_unit": null, + "postal_code": null, + "province": null, + "serial_number": null, + "street_address": null + } + ], + "uris": null, + "validity_end_time": "2026-10-01T10:57:29.687645506-05:00", + "validity_period_hours": 24, + "validity_start_time": "2026-09-30T10:57:29.687645506-05:00" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/main.tf b/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/main.tf new file mode 100644 index 00000000000..3c293accbb7 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/main.tf @@ -0,0 +1,55 @@ +resource "random_id" "suffix" { + byte_length = 2 +} + +locals { + prefix = "c7n-${terraform.workspace}-${random_id.suffix.hex}" +} + +resource "tls_private_key" "default" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "default" { + private_key_pem = tls_private_key.default.private_key_pem + validity_period_hours = 24 + allowed_uses = ["key_encipherment", "digital_signature", "server_auth"] + + subject { + common_name = "example.com" + } +} + +resource "google_compute_ssl_certificate" "default" { + name = "${local.prefix}-cert" + private_key = tls_private_key.default.private_key_pem + certificate = tls_self_signed_cert.default.cert_pem +} + +resource "google_compute_ssl_policy" "weak" { + name = "${local.prefix}-weak" + profile = "COMPATIBLE" +} + +resource "google_compute_health_check" "default" { + name = "${local.prefix}-hc" + + tcp_health_check { + port = 443 + } +} + +resource "google_compute_backend_service" "default" { + name = "${local.prefix}-bs" + protocol = "SSL" + load_balancing_scheme = "EXTERNAL" + health_checks = [google_compute_health_check.default.id] +} + +resource "google_compute_target_ssl_proxy" "weak" { + name = "${local.prefix}-ssl-weak" + backend_service = google_compute_backend_service.default.id + ssl_certificates = [google_compute_ssl_certificate.default.id] + ssl_policy = google_compute_ssl_policy.weak.id +} diff --git a/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/tf_resources.json b/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/tf_resources.json new file mode 100644 index 00000000000..96ae91dc729 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/lb_ssl_proxy_ssl_policy/tf_resources.json @@ -0,0 +1,235 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_compute_backend_service": { + "default": { + "affinity_cookie_ttl_sec": 0, + "backend": [], + "cdn_policy": [], + "circuit_breakers": [], + "compression_mode": "", + "connection_draining_timeout_sec": 300, + "consistent_hash": [], + "creation_timestamp": "2026-09-30T08:59:13.820-07:00", + "custom_metrics": [], + "custom_request_headers": null, + "custom_response_headers": null, + "deletion_policy": "DELETE", + "description": "", + "edge_security_policy": "", + "enable_cdn": false, + "external_managed_migration_state": "", + "external_managed_migration_testing_percentage": 0, + "fingerprint": "vXJbJt4CzBE=", + "generated_id": 3342100174151388990, + "health_checks": [ + "projects/cloud-custodian/global/healthChecks/c7n-default-3e3a-hc" + ], + "iap": [], + "id": "projects/cloud-custodian/global/backendServices/c7n-default-3e3a-bs", + "ip_address_selection_policy": "", + "load_balancing_scheme": "EXTERNAL", + "locality_lb_policies": [], + "locality_lb_policy": "", + "log_config": [], + "max_stream_duration": [], + "name": "c7n-default-3e3a-bs", + "outlier_detection": [], + "params": [], + "port_name": "http", + "project": "stkl-kody-mullins", + "protocol": "SSL", + "security_policy": "", + "security_settings": [], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/backendServices/c7n-default-3e3a-bs", + "service_lb_policy": "", + "session_affinity": "NONE", + "strong_session_affinity_cookie": [], + "timeout_sec": 30, + "timeouts": null, + "tls_settings": [] + } + }, + "google_compute_health_check": { + "default": { + "check_interval_sec": 5, + "creation_timestamp": "2026-09-30T08:59:02.551-07:00", + "deletion_policy": "DELETE", + "description": "", + "grpc_health_check": [], + "grpc_tls_health_check": [], + "healthy_threshold": 2, + "http2_health_check": [], + "http_health_check": [], + "https_health_check": [], + "id": "projects/cloud-custodian/global/healthChecks/c7n-default-3e3a-hc", + "log_config": [ + { + "enable": false + } + ], + "name": "c7n-default-3e3a-hc", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/healthChecks/c7n-default-3e3a-hc", + "source_regions": null, + "ssl_health_check": [], + "tcp_health_check": [ + { + "port": 443, + "port_name": "", + "port_specification": "", + "proxy_header": "NONE", + "request": "", + "response": "" + } + ], + "timeout_sec": 5, + "timeouts": null, + "type": "TCP", + "unhealthy_threshold": 2 + } + }, + "google_compute_ssl_certificate": { + "default": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIC6zCCAdOgAwIBAgIQSVqlpaoM1rF855e1uKOvHzANBgkqhkiG9w0BAQsFADAW\nMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0yNjA5MzAxNTU5MDFaFw0yNjEwMDEx\nNTU5MDFaMBYxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbVzDQQ\nbSGqRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxighXG\n4FlxJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod8LPs\nqK6/Fk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0SEQ2\nLfXhivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wzo8wX\nHkO8doXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQIDAQABozUwMzAOBgNVHQ8B\nAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkq\nhkiG9w0BAQsFAAOCAQEAmwIc5at5dceTzUANV7yu2XsqTI2EFeDezzdnQqqVHAJE\nFwuoynvNq0XCOHmNo0xWHaaiQNl5bgDHnZ4haSKNGqtnwNpmefhUA8fICKT9eHOk\nN93yeL2FzDb76Wqcc6ySXLJHMIWerrMrCn1DPDHgQvxvcBs5H9L1Mxu8gM8g3sC1\nuxLpxc5Pt9OOqPNK/MbZLKV6W94FFVtwni20Yo29WRwwEvT5M/zhATp52sexzorQ\nhUnQSA7RWeax+tp+BTGyh7gMHOk1gGxTdLVkuJfzd/Bfrp7c+Nw/hhaPDx9kLju6\nBSTLfzTsDVMd04oFiQnhYraGg9LKsUHU5vr/t+OqnQ==\n-----END CERTIFICATE-----\n", + "certificate_id": 7186965733733076777, + "creation_timestamp": "2026-09-30T08:59:02.549-07:00", + "deletion_policy": "DELETE", + "description": "", + "expire_time": "2026-10-01T08:59:01.000-07:00", + "id": "projects/cloud-custodian/global/sslCertificates/c7n-default-3e3a-cert", + "name": "c7n-default-3e3a-cert", + "name_prefix": null, + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbVzDQQbSGq\nRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxighXG4Flx\nJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod8LPsqK6/\nFk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0SEQ2LfXh\nivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wzo8wXHkO8\ndoXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQIDAQABAoIBAAZGxE/LtgoNah2y\novsAPF8IcoaVms/PqHsIwulLJvBfnGkhBdmgCWSUVg+/s7ddoKBy1haqzckgU+9F\nDaBXV7QaQbQSV2VRlDZLv5fDDV38UO8UEth9kSjfKpbBGW18kBVjPksBqrECEscu\nyQf5ieMxwFbRZjbNinecRZE1Ss3CturlM5bH7Kni7cjjoeOKpPOpIQEQ4ToeVh7i\ngf6vBvlyV/bC/I3aPHuvNZ3F4P4h0LW5Xmtu3YV04PVfxHFNgoUAV6sKxrBY/87E\nlIQodC7id/HYRRgZuatZxAIcOGapxGVxjwrInESTxrzmaJ/acSeDkY96s/Ks5nNo\nmNoHjIECgYEA5uiv3WQwEG0hd3YiflZBBASgKA2hhY9DP9AKQs1nTAgOEHZulb5Y\nfZmEWhMSB6k/CWx8ngOC1LFc/MRtMFhpA8PWT5o1bWsqnKZWTH6vgWwr3fLpo8Om\nXwD9tLxLM4LxuMwauGAZXj6fh0f/E2DS9EtetwBkBWzijrDiyg9M9ZkCgYEA2+QW\nJtAZ7pamjDWopDkz+zSwG6PEoCZ19sVdw85Z2zsoLp7i2m0ieAgHOeC+BnEFquJH\nI/lTts1P8GiV0GbaQxfQB2wCJ0DFCx9kv845U6WWK9bTyBER6hb/+sb6yjiNiqPX\nLaob1F+H1yc0I2YcTRHJq5sLJogJPWs6aDNi78ECgYBGWNFrQ2CZQIYXnPf5QVHh\nZX9ddHmm9PRp9nmJzIYR1cpjHZGnMSxIZ4K45mJxzxATUOfORAeRwM8xeq5BMk4c\nBYe/DJ4GTcXXn51fBKF3bnsWzTB84dhHzUd9zneio9v8jsH9NdnEqpNzZW3Ce5cJ\nMgUS/515QajsC9ur72lk0QKBgQC1aqrz/sFGXTiveR3Xz/IZEVEynJz+3Hx6yj8f\n60GrZFU0kZXsTBoiGc2MbOWafukEIYg8H9p3eVC4sjDYLeNpc14IoY3tQLzQ/meq\n78PQRU9uf+FesOas0akEB+KlsJ7mgWJopG4te1uhoRCdHAPBeil+0X4TniyxZaRc\nj/jSgQKBgCCKBokS0Mc9EsRuBLoQGHxscLJhA1eGe+P8PzY+4YLGKdfAReb2hCUH\nGyaGMFAvvjuqOCfFeb2THRV7ARXrH8+sgEmcoeFCR3+ab5dGiBVxedKaVKvHmPAq\n53XUDB+TlSIC7/25pUwdhCQQ538PA/TDgM/iE8nVmtmOSBbAMWM9\n-----END RSA PRIVATE KEY-----\n", + "private_key_wo": null, + "private_key_wo_version": "", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-3e3a-cert", + "timeouts": null + } + }, + "google_compute_ssl_policy": { + "weak": { + "creation_timestamp": "2026-09-30T08:59:02.633-07:00", + "custom_features": null, + "deletion_policy": "DELETE", + "description": "", + "enabled_features": [ + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_256_GCM_SHA384" + ], + "fingerprint": "UCiJtMU0f-Q=", + "id": "projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak", + "min_tls_version": "TLS_1_0", + "name": "c7n-default-3e3a-weak", + "post_quantum_key_exchange": "", + "profile": "COMPATIBLE", + "project": "stkl-kody-mullins", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak", + "timeouts": null + } + }, + "google_compute_target_ssl_proxy": { + "weak": { + "backend_service": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/backendServices/c7n-default-3e3a-bs", + "certificate_map": "", + "creation_timestamp": "2026-09-30T08:59:35.339-07:00", + "deletion_policy": "DELETE", + "description": "", + "id": "projects/cloud-custodian/global/targetSslProxies/c7n-default-3e3a-ssl-weak", + "name": "c7n-default-3e3a-ssl-weak", + "project": "stkl-kody-mullins", + "proxy_header": "NONE", + "proxy_id": 4588591286513078024, + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/targetSslProxies/c7n-default-3e3a-ssl-weak", + "ssl_certificates": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslCertificates/c7n-default-3e3a-cert" + ], + "ssl_policy": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/sslPolicies/c7n-default-3e3a-weak", + "timeouts": null + } + }, + "random_id": { + "suffix": { + "b64_std": "Pjo=", + "b64_url": "Pjo", + "byte_length": 2, + "dec": "15930", + "hex": "3e3a", + "id": "Pjo", + "keepers": null, + "prefix": null + } + }, + "tls_private_key": { + "default": { + "algorithm": "RSA", + "ecdsa_curve": "P224", + "id": "195768d04606d9bc8dc9233bff552e1294125646", + "private_key_openssh": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\nc2gtcnNhAAAAAwEAAQAAAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbV\nzDQQbSGqRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxi\nghXG4FlxJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod\n8LPsqK6/Fk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0\nSEQ2LfXhivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wz\no8wXHkO8doXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQAAA7iOOgWZjjoFmQAA\nAAdzc2gtcnNhAAABAQDGVsmqBz3tJkF04B+jPhVzHUW3KsoOuf7KE8OQNtXMNBBt\nIapFIcCRNjzNvepNyapyQgCzLW38IC8Fdev9cfLzMDrhGDh+G/uqCopK/GKCFcbg\nWXEmKZNzCHC67cjVdtP1GHPl3gmw3ilx7kWcJba/baCXTXi3ZgYg/Mgcah3ws+yo\nrr8WTjozU5zoN2dgwAk/7WvYOft+bl4J/8HgiwO1jf/v66v0peKkhW7Q47RIRDYt\n9eGK8KV6Ixj3PcHf2tYuXfRHvtiGvipnF2JDYWlcMG6yMzbxPhrgJdjj7DOjzBce\nQ7x2hdmnCVch1TVuWF+8BtmguSIGiIxXPynh4P9ZAAAAAwEAAQAAAQAGRsRPy7YK\nDWodsqL7ADxfCHKGlZrPz6h7CMLpSybwX5xpIQXZoAlklFYPv7O3XaCgctYWqs3J\nIFPvRQ2gV1e0GkG0EldlUZQ2S7+Xww1d/FDvFBLYfZEo3yqWwRltfJAVYz5LAaqx\nAhLHLskH+YnjMcBW0WY2zYp3nEWRNUrNwrbq5TOWx+yp4u3I46HjiqTzqSEBEOE6\nHlYe4oH+rwb5clf2wvyN2jx7rzWdxeD+IdC1uV5rbt2FdOD1X8RxTYKFAFerCsaw\nWP/OxJSEKHQu4nfx2EUYGbmrWcQCHDhmqcRlcY8KyJxEk8a85mif2nEng5GPerPy\nrOZzaJjaB4yBAAAAgCCKBokS0Mc9EsRuBLoQGHxscLJhA1eGe+P8PzY+4YLGKdfA\nReb2hCUHGyaGMFAvvjuqOCfFeb2THRV7ARXrH8+sgEmcoeFCR3+ab5dGiBVxedKa\nVKvHmPAq53XUDB+TlSIC7/25pUwdhCQQ538PA/TDgM/iE8nVmtmOSBbAMWM9AAAA\ngQDm6K/dZDAQbSF3diJ+VkEEBKAoDaGFj0M/0ApCzWdMCA4Qdm6Vvlh9mYRaExIH\nqT8JbHyeA4LUsVz8xG0wWGkDw9ZPmjVtayqcplZMfq+BbCvd8umjw6ZfAP20vEsz\ngvG4zBq4YBlePp+HR/8TYNL0S163AGQFbOKOsOLKD0z1mQAAAIEA2+QWJtAZ7pam\njDWopDkz+zSwG6PEoCZ19sVdw85Z2zsoLp7i2m0ieAgHOeC+BnEFquJHI/lTts1P\n8GiV0GbaQxfQB2wCJ0DFCx9kv845U6WWK9bTyBER6hb/+sb6yjiNiqPXLaob1F+H\n1yc0I2YcTRHJq5sLJogJPWs6aDNi78EAAAAAAQID\n-----END OPENSSH PRIVATE KEY-----\n", + "private_key_pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbVzDQQbSGq\nRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxighXG4Flx\nJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod8LPsqK6/\nFk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0SEQ2LfXh\nivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wzo8wXHkO8\ndoXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQIDAQABAoIBAAZGxE/LtgoNah2y\novsAPF8IcoaVms/PqHsIwulLJvBfnGkhBdmgCWSUVg+/s7ddoKBy1haqzckgU+9F\nDaBXV7QaQbQSV2VRlDZLv5fDDV38UO8UEth9kSjfKpbBGW18kBVjPksBqrECEscu\nyQf5ieMxwFbRZjbNinecRZE1Ss3CturlM5bH7Kni7cjjoeOKpPOpIQEQ4ToeVh7i\ngf6vBvlyV/bC/I3aPHuvNZ3F4P4h0LW5Xmtu3YV04PVfxHFNgoUAV6sKxrBY/87E\nlIQodC7id/HYRRgZuatZxAIcOGapxGVxjwrInESTxrzmaJ/acSeDkY96s/Ks5nNo\nmNoHjIECgYEA5uiv3WQwEG0hd3YiflZBBASgKA2hhY9DP9AKQs1nTAgOEHZulb5Y\nfZmEWhMSB6k/CWx8ngOC1LFc/MRtMFhpA8PWT5o1bWsqnKZWTH6vgWwr3fLpo8Om\nXwD9tLxLM4LxuMwauGAZXj6fh0f/E2DS9EtetwBkBWzijrDiyg9M9ZkCgYEA2+QW\nJtAZ7pamjDWopDkz+zSwG6PEoCZ19sVdw85Z2zsoLp7i2m0ieAgHOeC+BnEFquJH\nI/lTts1P8GiV0GbaQxfQB2wCJ0DFCx9kv845U6WWK9bTyBER6hb/+sb6yjiNiqPX\nLaob1F+H1yc0I2YcTRHJq5sLJogJPWs6aDNi78ECgYBGWNFrQ2CZQIYXnPf5QVHh\nZX9ddHmm9PRp9nmJzIYR1cpjHZGnMSxIZ4K45mJxzxATUOfORAeRwM8xeq5BMk4c\nBYe/DJ4GTcXXn51fBKF3bnsWzTB84dhHzUd9zneio9v8jsH9NdnEqpNzZW3Ce5cJ\nMgUS/515QajsC9ur72lk0QKBgQC1aqrz/sFGXTiveR3Xz/IZEVEynJz+3Hx6yj8f\n60GrZFU0kZXsTBoiGc2MbOWafukEIYg8H9p3eVC4sjDYLeNpc14IoY3tQLzQ/meq\n78PQRU9uf+FesOas0akEB+KlsJ7mgWJopG4te1uhoRCdHAPBeil+0X4TniyxZaRc\nj/jSgQKBgCCKBokS0Mc9EsRuBLoQGHxscLJhA1eGe+P8PzY+4YLGKdfAReb2hCUH\nGyaGMFAvvjuqOCfFeb2THRV7ARXrH8+sgEmcoeFCR3+ab5dGiBVxedKaVKvHmPAq\n53XUDB+TlSIC7/25pUwdhCQQ538PA/TDgM/iE8nVmtmOSBbAMWM9\n-----END RSA PRIVATE KEY-----\n", + "private_key_pem_pkcs8": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGVsmqBz3tJkF0\n4B+jPhVzHUW3KsoOuf7KE8OQNtXMNBBtIapFIcCRNjzNvepNyapyQgCzLW38IC8F\ndev9cfLzMDrhGDh+G/uqCopK/GKCFcbgWXEmKZNzCHC67cjVdtP1GHPl3gmw3ilx\n7kWcJba/baCXTXi3ZgYg/Mgcah3ws+yorr8WTjozU5zoN2dgwAk/7WvYOft+bl4J\n/8HgiwO1jf/v66v0peKkhW7Q47RIRDYt9eGK8KV6Ixj3PcHf2tYuXfRHvtiGvipn\nF2JDYWlcMG6yMzbxPhrgJdjj7DOjzBceQ7x2hdmnCVch1TVuWF+8BtmguSIGiIxX\nPynh4P9ZAgMBAAECggEABkbET8u2Cg1qHbKi+wA8XwhyhpWaz8+oewjC6Usm8F+c\naSEF2aAJZJRWD7+zt12goHLWFqrNySBT70UNoFdXtBpBtBJXZVGUNku/l8MNXfxQ\n7xQS2H2RKN8qlsEZbXyQFWM+SwGqsQISxy7JB/mJ4zHAVtFmNs2Kd5xFkTVKzcK2\n6uUzlsfsqeLtyOOh44qk86khARDhOh5WHuKB/q8G+XJX9sL8jdo8e681ncXg/iHQ\ntblea27dhXTg9V/EcU2ChQBXqwrGsFj/zsSUhCh0LuJ38dhFGBm5q1nEAhw4ZqnE\nZXGPCsicRJPGvOZon9pxJ4ORj3qz8qzmc2iY2geMgQKBgQDm6K/dZDAQbSF3diJ+\nVkEEBKAoDaGFj0M/0ApCzWdMCA4Qdm6Vvlh9mYRaExIHqT8JbHyeA4LUsVz8xG0w\nWGkDw9ZPmjVtayqcplZMfq+BbCvd8umjw6ZfAP20vEszgvG4zBq4YBlePp+HR/8T\nYNL0S163AGQFbOKOsOLKD0z1mQKBgQDb5BYm0BnulqaMNaikOTP7NLAbo8SgJnX2\nxV3DzlnbOygunuLabSJ4CAc54L4GcQWq4kcj+VO2zU/waJXQZtpDF9AHbAInQMUL\nH2S/zjlTpZYr1tPIERHqFv/6xvrKOI2Ko9ctqhvUX4fXJzQjZhxNEcmrmwsmiAk9\nazpoM2LvwQKBgEZY0WtDYJlAhhec9/lBUeFlf110eab09Gn2eYnMhhHVymMdkacx\nLEhngrjmYnHPEBNQ585EB5HAzzF6rkEyThwFh78MngZNxdefnV8EoXduexbNMHzh\n2EfNR33Od6Kj2/yOwf012cSqk3NlbcJ7lwkyBRL/nXlBqOwL26vvaWTRAoGBALVq\nqvP+wUZdOK95HdfP8hkRUTKcnP7cfHrKPx/rQatkVTSRlexMGiIZzYxs5Zp+6QQh\niDwf2nd5ULiyMNgt42lzXgihje1AvND+Z6rvw9BFT25/4V6w5qzRqQQH4qWwnuaB\nYmikbi17W6GhEJ0cA8F6KX7RfhOeLLFlpFyP+NKBAoGAIIoGiRLQxz0SxG4EuhAY\nfGxwsmEDV4Z74/w/Nj7hgsYp18BF5vaEJQcbJoYwUC++O6o4J8V5vZMdFXsBFesf\nz6yASZyh4UJHf5pvl0aIFXF50ppUq8eY8CrnddQMH5OVIgLv/bmlTB2EJBDnfw8D\n9MOAz+ITydWa2Y5IFsAxYz0=\n-----END PRIVATE KEY-----\n", + "public_key_fingerprint_md5": "b6:e0:7c:0c:c4:e5:4c:61:1a:0c:df:36:4f:4d:04:51", + "public_key_fingerprint_sha256": "SHA256:NgC8G1Dm4Ol93v36VC1wzG4jjg06eSf57fSGGFZ12Lk", + "public_key_openssh": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDGVsmqBz3tJkF04B+jPhVzHUW3KsoOuf7KE8OQNtXMNBBtIapFIcCRNjzNvepNyapyQgCzLW38IC8Fdev9cfLzMDrhGDh+G/uqCopK/GKCFcbgWXEmKZNzCHC67cjVdtP1GHPl3gmw3ilx7kWcJba/baCXTXi3ZgYg/Mgcah3ws+yorr8WTjozU5zoN2dgwAk/7WvYOft+bl4J/8HgiwO1jf/v66v0peKkhW7Q47RIRDYt9eGK8KV6Ixj3PcHf2tYuXfRHvtiGvipnF2JDYWlcMG6yMzbxPhrgJdjj7DOjzBceQ7x2hdmnCVch1TVuWF+8BtmguSIGiIxXPynh4P9Z\n", + "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlbJqgc97SZBdOAfoz4V\ncx1FtyrKDrn+yhPDkDbVzDQQbSGqRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy\n8zA64Rg4fhv7qgqKSvxighXG4FlxJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2\nv22gl014t2YGIPzIHGod8LPsqK6/Fk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsD\ntY3/7+ur9KXipIVu0OO0SEQ2LfXhivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2Fp\nXDBusjM28T4a4CXY4+wzo8wXHkO8doXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/\nWQIDAQAB\n-----END PUBLIC KEY-----\n", + "rsa_bits": 2048 + } + }, + "tls_self_signed_cert": { + "default": { + "allowed_uses": [ + "key_encipherment", + "digital_signature", + "server_auth" + ], + "cert_pem": "-----BEGIN CERTIFICATE-----\nMIIC6zCCAdOgAwIBAgIQSVqlpaoM1rF855e1uKOvHzANBgkqhkiG9w0BAQsFADAW\nMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0yNjA5MzAxNTU5MDFaFw0yNjEwMDEx\nNTU5MDFaMBYxFDASBgNVBAMTC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbVzDQQ\nbSGqRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxighXG\n4FlxJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod8LPs\nqK6/Fk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0SEQ2\nLfXhivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wzo8wX\nHkO8doXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQIDAQABozUwMzAOBgNVHQ8B\nAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkq\nhkiG9w0BAQsFAAOCAQEAmwIc5at5dceTzUANV7yu2XsqTI2EFeDezzdnQqqVHAJE\nFwuoynvNq0XCOHmNo0xWHaaiQNl5bgDHnZ4haSKNGqtnwNpmefhUA8fICKT9eHOk\nN93yeL2FzDb76Wqcc6ySXLJHMIWerrMrCn1DPDHgQvxvcBs5H9L1Mxu8gM8g3sC1\nuxLpxc5Pt9OOqPNK/MbZLKV6W94FFVtwni20Yo29WRwwEvT5M/zhATp52sexzorQ\nhUnQSA7RWeax+tp+BTGyh7gMHOk1gGxTdLVkuJfzd/Bfrp7c+Nw/hhaPDx9kLju6\nBSTLfzTsDVMd04oFiQnhYraGg9LKsUHU5vr/t+OqnQ==\n-----END CERTIFICATE-----\n", + "dns_names": null, + "early_renewal_hours": 0, + "id": "97504310132426383227746715872350416671", + "ip_addresses": null, + "is_ca_certificate": false, + "key_algorithm": "RSA", + "max_path_length": null, + "private_key_pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAxlbJqgc97SZBdOAfoz4Vcx1FtyrKDrn+yhPDkDbVzDQQbSGq\nRSHAkTY8zb3qTcmqckIAsy1t/CAvBXXr/XHy8zA64Rg4fhv7qgqKSvxighXG4Flx\nJimTcwhwuu3I1XbT9Rhz5d4JsN4pce5FnCW2v22gl014t2YGIPzIHGod8LPsqK6/\nFk46M1Oc6DdnYMAJP+1r2Dn7fm5eCf/B4IsDtY3/7+ur9KXipIVu0OO0SEQ2LfXh\nivCleiMY9z3B39rWLl30R77Yhr4qZxdiQ2FpXDBusjM28T4a4CXY4+wzo8wXHkO8\ndoXZpwlXIdU1blhfvAbZoLkiBoiMVz8p4eD/WQIDAQABAoIBAAZGxE/LtgoNah2y\novsAPF8IcoaVms/PqHsIwulLJvBfnGkhBdmgCWSUVg+/s7ddoKBy1haqzckgU+9F\nDaBXV7QaQbQSV2VRlDZLv5fDDV38UO8UEth9kSjfKpbBGW18kBVjPksBqrECEscu\nyQf5ieMxwFbRZjbNinecRZE1Ss3CturlM5bH7Kni7cjjoeOKpPOpIQEQ4ToeVh7i\ngf6vBvlyV/bC/I3aPHuvNZ3F4P4h0LW5Xmtu3YV04PVfxHFNgoUAV6sKxrBY/87E\nlIQodC7id/HYRRgZuatZxAIcOGapxGVxjwrInESTxrzmaJ/acSeDkY96s/Ks5nNo\nmNoHjIECgYEA5uiv3WQwEG0hd3YiflZBBASgKA2hhY9DP9AKQs1nTAgOEHZulb5Y\nfZmEWhMSB6k/CWx8ngOC1LFc/MRtMFhpA8PWT5o1bWsqnKZWTH6vgWwr3fLpo8Om\nXwD9tLxLM4LxuMwauGAZXj6fh0f/E2DS9EtetwBkBWzijrDiyg9M9ZkCgYEA2+QW\nJtAZ7pamjDWopDkz+zSwG6PEoCZ19sVdw85Z2zsoLp7i2m0ieAgHOeC+BnEFquJH\nI/lTts1P8GiV0GbaQxfQB2wCJ0DFCx9kv845U6WWK9bTyBER6hb/+sb6yjiNiqPX\nLaob1F+H1yc0I2YcTRHJq5sLJogJPWs6aDNi78ECgYBGWNFrQ2CZQIYXnPf5QVHh\nZX9ddHmm9PRp9nmJzIYR1cpjHZGnMSxIZ4K45mJxzxATUOfORAeRwM8xeq5BMk4c\nBYe/DJ4GTcXXn51fBKF3bnsWzTB84dhHzUd9zneio9v8jsH9NdnEqpNzZW3Ce5cJ\nMgUS/515QajsC9ur72lk0QKBgQC1aqrz/sFGXTiveR3Xz/IZEVEynJz+3Hx6yj8f\n60GrZFU0kZXsTBoiGc2MbOWafukEIYg8H9p3eVC4sjDYLeNpc14IoY3tQLzQ/meq\n78PQRU9uf+FesOas0akEB+KlsJ7mgWJopG4te1uhoRCdHAPBeil+0X4TniyxZaRc\nj/jSgQKBgCCKBokS0Mc9EsRuBLoQGHxscLJhA1eGe+P8PzY+4YLGKdfAReb2hCUH\nGyaGMFAvvjuqOCfFeb2THRV7ARXrH8+sgEmcoeFCR3+ab5dGiBVxedKaVKvHmPAq\n53XUDB+TlSIC7/25pUwdhCQQ538PA/TDgM/iE8nVmtmOSBbAMWM9\n-----END RSA PRIVATE KEY-----\n", + "private_key_pem_wo": null, + "private_key_pem_wo_version": null, + "ready_for_renewal": false, + "set_authority_key_id": false, + "set_subject_key_id": false, + "subject": [ + { + "common_name": "example.com", + "country": null, + "email_address": null, + "locality": null, + "organization": null, + "organizational_unit": null, + "postal_code": null, + "province": null, + "serial_number": null, + "street_address": null + } + ], + "uris": null, + "validity_end_time": "2026-10-01T10:59:01.836068853-05:00", + "validity_period_hours": 24, + "validity_start_time": "2026-09-30T10:59:01.836068853-05:00" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_loadbalancer.py b/tools/c7n_gcp/tests/test_loadbalancer.py index 5088e28dd34..5cfd22b826e 100644 --- a/tools/c7n_gcp/tests/test_loadbalancer.py +++ b/tools/c7n_gcp/tests/test_loadbalancer.py @@ -1,8 +1,14 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import logging from time import sleep +from unittest import mock + from gcp_common import BaseTest, event_data +from googleapiclient.errors import HttpError +from httplib2 import Response from pytest_terraform import terraform +from c7n_gcp.client import ServiceClient from c7n_gcp.resources.loadbalancer import LoadBalancingAddress from c7n_gcp.resources.loadbalancer import LoadBalancingGlobalAddress from c7n_gcp.resources.loadbalancer import LoadBalancingGlobalForwardingRule @@ -1036,3 +1042,91 @@ def test_loadbalancer_global_forwarding_rule_labels( resource = LoadBalancingGlobalForwardingRule.resource_type.refresh(client, resources[0]) assert resource['labels']['env'] == 'not-the-default' assert resource['labelFingerprint'] != label_fingerprint + + +def _run_ssl_policy_filter(test, resource, flight, names, ssl_filter=None): + if ssl_filter is None: + ssl_filter = {'type': 'ssl-policy', 'key': 'profile', 'value': 'COMPATIBLE'} + factory = test.replay_flight_data(flight) + policy = test.load_policy( + { + 'name': flight, + 'resource': resource, + 'filters': [ + {'type': 'value', 'key': 'name', 'op': 'in', 'value': names}, + ssl_filter, + ], + }, + session_factory=factory, + ) + return policy.run() + + +@terraform('lb_https_proxy_ssl_policy', scope='session') +def test_lb_https_proxy_ssl_policy_good(test, lb_https_proxy_ssl_policy): + proxy = lb_https_proxy_ssl_policy.resources['google_compute_target_https_proxy']['good'] + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-https-proxy', 'lb-https-ssl-good', + [proxy['name']]) + assert resources == [] + + +@terraform('lb_https_proxy_ssl_policy', scope='session') +def test_lb_https_proxy_ssl_policy_none(test, lb_https_proxy_ssl_policy): + proxy = lb_https_proxy_ssl_policy.resources['google_compute_target_https_proxy']['none'] + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-https-proxy', 'lb-https-ssl-none', + [proxy['name']]) + assert resources == [] + + +@terraform('lb_https_proxy_ssl_policy', scope='session') +def test_lb_https_proxy_ssl_policy_shared(test, lb_https_proxy_ssl_policy): + # Both proxies use the same ssl policy, which is only fetched once. + proxies = lb_https_proxy_ssl_policy.resources['google_compute_target_https_proxy'] + names = [proxies['weak']['name'], proxies['weak2']['name']] + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-https-proxy', 'lb-https-ssl-shared', + names) + assert sorted(r['name'] for r in resources) == sorted(names) + assert all(r['c7n:ssl-policy']['profile'] == 'COMPATIBLE' for r in resources) + + +@terraform('lb_https_proxy_ssl_policy', scope='session') +def test_lb_https_proxy_ssl_policy_not_found(test, lb_https_proxy_ssl_policy, caplog): + # A 404 happens when the policy is deleted after the proxies were listed + # (or cached). GCP won't delete an in-use policy, so it can't be recorded + # live. Replay the proxy list and fail the ssl policy get. + proxies = lb_https_proxy_ssl_policy.resources['google_compute_target_https_proxy'] + names = [proxies['weak']['name'], proxies['weak2']['name']] + not_found = HttpError(Response({'status': 404}), b'not found') + with mock.patch.object( + ServiceClient, 'execute_command', side_effect=not_found) as get: + with caplog.at_level(logging.WARNING, logger='custodian.gcp.loadbalancer'): + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-https-proxy', 'lb-https-ssl-shared', + names) + assert resources == [] + assert get.call_count == 1 + assert 'ssl policy not found' in caplog.text + + +@terraform('lb_https_proxy_ssl_policy', scope='session') +def test_lb_https_proxy_ssl_policy_not_none(test, lb_https_proxy_ssl_policy): + # A proxy with no sslPolicy fails the ssl-policy check, so not matches it. + proxy = lb_https_proxy_ssl_policy.resources['google_compute_target_https_proxy']['none'] + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-https-proxy', 'lb-https-ssl-not-none', + [proxy['name']], + ssl_filter={'not': [{'type': 'ssl-policy', 'key': 'profile', 'value': 'MODERN'}]}) + assert [r['name'] for r in resources] == [proxy['name']] + + +@terraform('lb_ssl_proxy_ssl_policy', scope='session') +def test_lb_ssl_proxy_ssl_policy_weak(test, lb_ssl_proxy_ssl_policy): + proxy = lb_ssl_proxy_ssl_policy.resources['google_compute_target_ssl_proxy']['weak'] + resources = _run_ssl_policy_filter( + test, 'gcp.loadbalancer-target-ssl-proxy', 'lb-sslproxy-ssl-weak', + [proxy['name']]) + assert [r['name'] for r in resources] == [proxy['name']] + assert resources[0]['c7n:ssl-policy']['profile'] == 'COMPATIBLE' From 9105ca8f9916bf8c915a8abc0e224174598684cd Mon Sep 17 00:00:00 2001 From: scott-sixfeetup Date: Wed, 14 Oct 2026 11:27:46 -0400 Subject: [PATCH 41/65] aws - kafka - add kafka-broker child resource with metrics filter (#10481) --------- Co-authored-by: AJ Kerrigan Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- c7n/resources/kafka.py | 85 +++++- c7n/resources/resource_map.py | 1 + .../kafka.ListClustersV2_1.json | 99 ++++++ .../kafka.ListNodes_1.json | 44 +++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_2.json | 23 ++ .../kafka.ListClustersV2_1.json | 99 ++++++ .../kafka.ListNodes_1.json | 44 +++ tests/terraform/kafka_broker/main.tf | 43 +++ tests/terraform/kafka_broker/network.tf | 49 +++ tests/terraform/kafka_broker/providers.tf | 17 ++ .../terraform/kafka_broker/tf_resources.json | 285 ++++++++++++++++++ tests/test_kafka.py | 42 +++ tests/test_policy.py | 3 +- 14 files changed, 855 insertions(+), 2 deletions(-) create mode 100644 tests/data/placebo/test_kafka_broker_metrics/kafka.ListClustersV2_1.json create mode 100644 tests/data/placebo/test_kafka_broker_metrics/kafka.ListNodes_1.json create mode 100644 tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_kafka_broker_query/kafka.ListClustersV2_1.json create mode 100644 tests/data/placebo/test_kafka_broker_query/kafka.ListNodes_1.json create mode 100644 tests/terraform/kafka_broker/main.tf create mode 100644 tests/terraform/kafka_broker/network.tf create mode 100644 tests/terraform/kafka_broker/providers.tf create mode 100644 tests/terraform/kafka_broker/tf_resources.json diff --git a/c7n/resources/kafka.py b/c7n/resources/kafka.py index 2010df2178c..1cd9d3ad87c 100644 --- a/c7n/resources/kafka.py +++ b/c7n/resources/kafka.py @@ -5,12 +5,16 @@ from c7n.filters.vpc import SecurityGroupFilter, SubnetFilter from c7n.manager import resources from c7n.filters.kms import KmsRelatedFilter -from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, ConfigSource +from c7n.query import ( + QueryResourceManager, TypeInfo, DescribeSource, ConfigSource, + ChildResourceManager, ChildDescribeSource +) from c7n.utils import local_session, type_schema from c7n.vendored.distutils.version import LooseVersion from .aws import shape_validate from c7n.filters import CrossAccountAccessFilter +from c7n.filters.metrics import MetricsFilter class DescribeKafka(DescribeSource): @@ -54,6 +58,85 @@ class resource_type(TypeInfo): } +class DescribeKafkaBroker(ChildDescribeSource): + """Source for enumerating Kafka broker nodes.""" + + def get_query(self): + return super().get_query(capture_parent_id=True) + + def augment(self, resources): + """Augment broker resources with cluster info for metrics dimensions.""" + results = [] + for cluster_arn, node_info in resources: + node_info['ClusterArn'] = cluster_arn + parts = cluster_arn.split('/') + if len(parts) >= 2: + node_info['ClusterName'] = parts[1] + if 'BrokerNodeInfo' in node_info: + node_info['BrokerId'] = node_info['BrokerNodeInfo'].get('BrokerId') + results.append(node_info) + return results + + +@resources.register('kafka-broker') +class KafkaBroker(ChildResourceManager): + """Resource manager for MSK Kafka broker nodes. + + Broker nodes are only available for provisioned clusters. + Serverless clusters do not have enumerable broker nodes. + """ + + class resource_type(TypeInfo): + service = 'kafka' + parent_spec = ('kafka', 'ClusterArn', True) + enum_spec = ('list_nodes', 'NodeInfoList', None) + permissions_enum = ('kafka:ListClustersV2', 'kafka:ListNodes') + name = id = 'BrokerId' + arn = False + dimension = 'Broker ID' + metrics_namespace = 'AWS/Kafka' + + source_mapping = { + 'describe-child': DescribeKafkaBroker, + 'describe': DescribeKafkaBroker, + } + + +@KafkaBroker.filter_registry.register('metrics') +class KafkaBrokerMetrics(MetricsFilter): + """CloudWatch metrics filter for Kafka broker nodes. + + Supports broker-level metrics like CpuUser, CpuSystem, KafkaDataLogsDiskUsed, etc. + + :example: + + Find brokers with low CPU utilization over 14 days: + + .. code-block:: yaml + + policies: + - name: underutilized-kafka-brokers + resource: aws.kafka-broker + filters: + - type: metrics + name: CpuUser + days: 14 + value: 10 + op: less-than + """ + + def get_dimensions(self, resource): + """Return CloudWatch dimensions for MSK broker metrics. + + MSK broker-level metrics require both 'Cluster Name' and 'Broker ID' dimensions. + Note: BrokerId comes from AWS as a float (e.g., 1.0), but CloudWatch expects "1". + """ + return [ + {'Name': 'Cluster Name', 'Value': resource.get('ClusterName', '')}, + {'Name': 'Broker ID', 'Value': str(int(resource.get('BrokerId', 0)))} + ] + + @Kafka.filter_registry.register('security-group') class KafkaSGFilter(SecurityGroupFilter): diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py index ab77b6c3e24..84ca5ff3b38 100644 --- a/c7n/resources/resource_map.py +++ b/c7n/resources/resource_map.py @@ -213,6 +213,7 @@ "aws.iot-ota-update": "c7n.resources.iot.IoTOTAUpdate", "aws.iot-policy": "c7n.resources.iot.IoTPolicy", "aws.kafka": "c7n.resources.kafka.Kafka", + "aws.kafka-broker": "c7n.resources.kafka.KafkaBroker", "aws.kafka-config": "c7n.resources.kafka.KafkaClusterConfiguration", "aws.kendra": "c7n.resources.kendra.KendraIndex", "aws.key-pair": "c7n.resources.vpc.KeyPair", diff --git a/tests/data/placebo/test_kafka_broker_metrics/kafka.ListClustersV2_1.json b/tests/data/placebo/test_kafka_broker_metrics/kafka.ListClustersV2_1.json new file mode 100644 index 00000000000..1426fce1e91 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_metrics/kafka.ListClustersV2_1.json @@ -0,0 +1,99 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ClusterInfoList": [ + { + "ClusterType": "PROVISIONED", + "ClusterArn": "arn:aws:kafka:us-east-1:644160558196:cluster/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18", + "ClusterName": "c7n-test-48042afe", + "CreationTime": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 17, + "hour": 14, + "minute": 29, + "second": 34, + "microsecond": 109000 + }, + "CurrentVersion": "K3P5ROKL5A1OLE", + "State": "ACTIVE", + "Tags": { + "Environment": "test", + "Name": "c7n-test-msk-48042afe" + }, + "Provisioned": { + "BrokerNodeGroupInfo": { + "BrokerAZDistribution": "DEFAULT", + "ClientSubnets": [ + "subnet-0b9fcdaeca4e2a0c3", + "subnet-0599c09bf908c27f0" + ], + "InstanceType": "kafka.t3.small", + "SecurityGroups": [ + "sg-0a0a8ec3122a2e004" + ], + "StorageInfo": { + "EbsStorageInfo": { + "VolumeSize": 10 + } + }, + "ConnectivityInfo": { + "PublicAccess": { + "Type": "DISABLED" + }, + "VpcConnectivity": { + "ClientAuthentication": { + "Sasl": { + "Scram": { + "Enabled": false + }, + "Iam": { + "Enabled": false + } + }, + "Tls": { + "Enabled": false + } + } + } + }, + "ZoneIds": [ + "use1-az6", + "use1-az1" + ] + }, + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "EncryptionInfo": { + "EncryptionAtRest": { + "DataVolumeKMSKeyId": "arn:aws:kms:us-east-1:644160558196:key/8aa4e58d-6208-4292-a711-3c79f65d5d5e" + }, + "EncryptionInTransit": { + "ClientBroker": "TLS", + "InCluster": true + } + }, + "EnhancedMonitoring": "PER_BROKER", + "OpenMonitoring": { + "Prometheus": { + "JmxExporter": { + "EnabledInBroker": false + }, + "NodeExporter": { + "EnabledInBroker": false + } + } + }, + "NumberOfBrokerNodes": 2, + "ZookeeperConnectString": "z-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181,z-3.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181,z-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181", + "ZookeeperConnectStringTls": "z-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182,z-3.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182,z-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182", + "StorageMode": "LOCAL", + "CustomerActionStatus": "NONE" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_kafka_broker_metrics/kafka.ListNodes_1.json b/tests/data/placebo/test_kafka_broker_metrics/kafka.ListNodes_1.json new file mode 100644 index 00000000000..4d50a83be70 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_metrics/kafka.ListNodes_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "NodeInfoList": [ + { + "AddedToClusterTime": "2025-12-17T14:40:07.292Z", + "BrokerNodeInfo": { + "AttachedENIId": "eni-0270d8e74b03a8bac", + "BrokerId": 2.0, + "ClientSubnet": "subnet-0599c09bf908c27f0", + "ClientVpcIpAddress": "10.0.1.95", + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "Endpoints": [ + "b-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com" + ] + }, + "InstanceType": "t3.small", + "NodeARN": "arn:aws:kafka:us-east-1:644160558196:broker/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18/257c02b9-11ac-4f8e-b568-c84b38a6f241", + "NodeType": "BROKER" + }, + { + "AddedToClusterTime": "2025-12-17T14:40:07.251Z", + "BrokerNodeInfo": { + "AttachedENIId": "eni-0fc6fcc0c6d606b63", + "BrokerId": 1.0, + "ClientSubnet": "subnet-0b9fcdaeca4e2a0c3", + "ClientVpcIpAddress": "10.0.0.240", + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "Endpoints": [ + "b-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com" + ] + }, + "InstanceType": "t3.small", + "NodeARN": "arn:aws:kafka:us-east-1:644160558196:broker/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18/e0a96a8d-40ad-4c47-ad69-066ac790f6aa", + "NodeType": "BROKER" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..3c550fbd526 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CpuUser", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 3, + "hour": 15, + "minute": 31, + "second": 0, + "microsecond": 0 + }, + "Average": 4.5605654198000005, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..3ddf7ae4889 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_metrics/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CpuUser", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 3, + "hour": 15, + "minute": 31, + "second": 0, + "microsecond": 0 + }, + "Average": 4.719833856200002, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_kafka_broker_query/kafka.ListClustersV2_1.json b/tests/data/placebo/test_kafka_broker_query/kafka.ListClustersV2_1.json new file mode 100644 index 00000000000..1426fce1e91 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_query/kafka.ListClustersV2_1.json @@ -0,0 +1,99 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "ClusterInfoList": [ + { + "ClusterType": "PROVISIONED", + "ClusterArn": "arn:aws:kafka:us-east-1:644160558196:cluster/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18", + "ClusterName": "c7n-test-48042afe", + "CreationTime": { + "__class__": "datetime", + "year": 2025, + "month": 12, + "day": 17, + "hour": 14, + "minute": 29, + "second": 34, + "microsecond": 109000 + }, + "CurrentVersion": "K3P5ROKL5A1OLE", + "State": "ACTIVE", + "Tags": { + "Environment": "test", + "Name": "c7n-test-msk-48042afe" + }, + "Provisioned": { + "BrokerNodeGroupInfo": { + "BrokerAZDistribution": "DEFAULT", + "ClientSubnets": [ + "subnet-0b9fcdaeca4e2a0c3", + "subnet-0599c09bf908c27f0" + ], + "InstanceType": "kafka.t3.small", + "SecurityGroups": [ + "sg-0a0a8ec3122a2e004" + ], + "StorageInfo": { + "EbsStorageInfo": { + "VolumeSize": 10 + } + }, + "ConnectivityInfo": { + "PublicAccess": { + "Type": "DISABLED" + }, + "VpcConnectivity": { + "ClientAuthentication": { + "Sasl": { + "Scram": { + "Enabled": false + }, + "Iam": { + "Enabled": false + } + }, + "Tls": { + "Enabled": false + } + } + } + }, + "ZoneIds": [ + "use1-az6", + "use1-az1" + ] + }, + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "EncryptionInfo": { + "EncryptionAtRest": { + "DataVolumeKMSKeyId": "arn:aws:kms:us-east-1:644160558196:key/8aa4e58d-6208-4292-a711-3c79f65d5d5e" + }, + "EncryptionInTransit": { + "ClientBroker": "TLS", + "InCluster": true + } + }, + "EnhancedMonitoring": "PER_BROKER", + "OpenMonitoring": { + "Prometheus": { + "JmxExporter": { + "EnabledInBroker": false + }, + "NodeExporter": { + "EnabledInBroker": false + } + } + }, + "NumberOfBrokerNodes": 2, + "ZookeeperConnectString": "z-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181,z-3.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181,z-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2181", + "ZookeeperConnectStringTls": "z-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182,z-3.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182,z-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com:2182", + "StorageMode": "LOCAL", + "CustomerActionStatus": "NONE" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_kafka_broker_query/kafka.ListNodes_1.json b/tests/data/placebo/test_kafka_broker_query/kafka.ListNodes_1.json new file mode 100644 index 00000000000..4d50a83be70 --- /dev/null +++ b/tests/data/placebo/test_kafka_broker_query/kafka.ListNodes_1.json @@ -0,0 +1,44 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "NodeInfoList": [ + { + "AddedToClusterTime": "2025-12-17T14:40:07.292Z", + "BrokerNodeInfo": { + "AttachedENIId": "eni-0270d8e74b03a8bac", + "BrokerId": 2.0, + "ClientSubnet": "subnet-0599c09bf908c27f0", + "ClientVpcIpAddress": "10.0.1.95", + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "Endpoints": [ + "b-2.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com" + ] + }, + "InstanceType": "t3.small", + "NodeARN": "arn:aws:kafka:us-east-1:644160558196:broker/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18/257c02b9-11ac-4f8e-b568-c84b38a6f241", + "NodeType": "BROKER" + }, + { + "AddedToClusterTime": "2025-12-17T14:40:07.251Z", + "BrokerNodeInfo": { + "AttachedENIId": "eni-0fc6fcc0c6d606b63", + "BrokerId": 1.0, + "ClientSubnet": "subnet-0b9fcdaeca4e2a0c3", + "ClientVpcIpAddress": "10.0.0.240", + "CurrentBrokerSoftwareInfo": { + "KafkaVersion": "3.5.1" + }, + "Endpoints": [ + "b-1.c7ntest48042afe.sidyyb.c18.kafka.us-east-1.amazonaws.com" + ] + }, + "InstanceType": "t3.small", + "NodeARN": "arn:aws:kafka:us-east-1:644160558196:broker/c7n-test-48042afe/7a89d361-f81d-4c9a-bf97-53518cfc715d-18/e0a96a8d-40ad-4c47-ad69-066ac790f6aa", + "NodeType": "BROKER" + } + ] + } +} \ No newline at end of file diff --git a/tests/terraform/kafka_broker/main.tf b/tests/terraform/kafka_broker/main.tf new file mode 100644 index 00000000000..ba3bfc56fad --- /dev/null +++ b/tests/terraform/kafka_broker/main.tf @@ -0,0 +1,43 @@ +resource "random_id" "id" { + byte_length = 4 +} + +# MSK Cluster - using smallest instance type for cost efficiency +resource "aws_msk_cluster" "test" { + cluster_name = "c7n-test-${random_id.id.hex}" + kafka_version = "3.5.1" + number_of_broker_nodes = 2 + + broker_node_group_info { + instance_type = "kafka.t3.small" + client_subnets = aws_subnet.msk[*].id + security_groups = [aws_security_group.msk.id] + + storage_info { + ebs_storage_info { + volume_size = 10 + } + } + } + + # Enable enhanced monitoring for metrics testing + enhanced_monitoring = "PER_BROKER" + + tags = { + Name = "c7n-test-msk-${random_id.id.hex}" + Environment = "test" + } +} + +output "cluster_arn" { + value = aws_msk_cluster.test.arn +} + +output "cluster_name" { + value = aws_msk_cluster.test.cluster_name +} + +output "bootstrap_brokers" { + value = aws_msk_cluster.test.bootstrap_brokers +} + diff --git a/tests/terraform/kafka_broker/network.tf b/tests/terraform/kafka_broker/network.tf new file mode 100644 index 00000000000..3a9bf3e70e7 --- /dev/null +++ b/tests/terraform/kafka_broker/network.tf @@ -0,0 +1,49 @@ +resource "aws_vpc" "msk" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "msk-test-vpc-${random_id.id.hex}" + } +} + +data "aws_availability_zones" "available" { + state = "available" +} + +resource "aws_subnet" "msk" { + count = 2 + + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.msk.cidr_block, 8, count.index) + vpc_id = aws_vpc.msk.id + + tags = { + Name = "msk-test-subnet-${count.index}-${random_id.id.hex}" + } +} + +resource "aws_security_group" "msk" { + name_prefix = "msk-test-sg-" + vpc_id = aws_vpc.msk.id + + ingress { + from_port = 9092 + to_port = 9098 + protocol = "tcp" + cidr_blocks = [aws_vpc.msk.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "msk-test-sg-${random_id.id.hex}" + } +} + diff --git a/tests/terraform/kafka_broker/providers.tf b/tests/terraform/kafka_broker/providers.tf new file mode 100644 index 00000000000..b45396df731 --- /dev/null +++ b/tests/terraform/kafka_broker/providers.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +provider "aws" { + region = "us-east-1" +} + diff --git a/tests/terraform/kafka_broker/tf_resources.json b/tests/terraform/kafka_broker/tf_resources.json new file mode 100644 index 00000000000..4a582ae92b9 --- /dev/null +++ b/tests/terraform/kafka_broker/tf_resources.json @@ -0,0 +1,285 @@ +{ + "pytest-terraform": 1, + "outputs": { + "bootstrap_brokers": { + "value": "", + "type": "string" + }, + "cluster_arn": { + "value": "arn:aws:kafka:us-east-1:644160558196:cluster/c7n-test-da399252/97ace2be-cf6d-467c-bc6a-e61df089f6c2-18", + "type": "string" + }, + "cluster_name": { + "value": "c7n-test-da399252", + "type": "string" + } + }, + "resources": { + "aws_availability_zones": { + "available": { + "all_availability_zones": null, + "exclude_names": null, + "exclude_zone_ids": null, + "filter": null, + "group_names": [ + "us-east-1-zg-1" + ], + "id": "us-east-1", + "names": [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ], + "state": "available", + "timeouts": null, + "zone_ids": [ + "use1-az6", + "use1-az1", + "use1-az2", + "use1-az4", + "use1-az3", + "use1-az5" + ] + } + }, + "aws_msk_cluster": { + "test": { + "arn": "arn:aws:kafka:us-east-1:644160558196:cluster/c7n-test-da399252/97ace2be-cf6d-467c-bc6a-e61df089f6c2-18", + "bootstrap_brokers": "", + "bootstrap_brokers_public_sasl_iam": "", + "bootstrap_brokers_public_sasl_scram": "", + "bootstrap_brokers_public_tls": "", + "bootstrap_brokers_sasl_iam": "", + "bootstrap_brokers_sasl_scram": "", + "bootstrap_brokers_tls": "b-1.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:9094,b-2.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:9094", + "bootstrap_brokers_vpc_connectivity_sasl_iam": "", + "bootstrap_brokers_vpc_connectivity_sasl_scram": "", + "bootstrap_brokers_vpc_connectivity_tls": "", + "broker_node_group_info": [ + { + "az_distribution": "DEFAULT", + "client_subnets": [ + "subnet-0276a17ec8e0ad42f", + "subnet-02e879cee93508233" + ], + "connectivity_info": [ + { + "public_access": [ + { + "type": "DISABLED" + } + ], + "vpc_connectivity": [ + { + "client_authentication": [ + { + "sasl": [ + { + "iam": false, + "scram": false + } + ], + "tls": false + } + ] + } + ] + } + ], + "instance_type": "kafka.t3.small", + "security_groups": [ + "sg-049dfc123dab473e1" + ], + "storage_info": [ + { + "ebs_storage_info": [ + { + "provisioned_throughput": [], + "volume_size": 10 + } + ] + } + ] + } + ], + "client_authentication": [], + "cluster_name": "c7n-test-da399252", + "cluster_uuid": "97ace2be-cf6d-467c-bc6a-e61df089f6c2-18", + "configuration_info": [], + "current_version": "K3P5ROKL5A1OLE", + "encryption_info": [ + { + "encryption_at_rest_kms_key_arn": "arn:aws:kms:us-east-1:644160558196:key/8aa4e58d-6208-4292-a711-3c79f65d5d5e", + "encryption_in_transit": [ + { + "client_broker": "TLS", + "in_cluster": true + } + ] + } + ], + "enhanced_monitoring": "PER_BROKER", + "id": "arn:aws:kafka:us-east-1:644160558196:cluster/c7n-test-da399252/97ace2be-cf6d-467c-bc6a-e61df089f6c2-18", + "kafka_version": "3.5.1", + "logging_info": [], + "number_of_broker_nodes": 2, + "open_monitoring": [ + { + "prometheus": [ + { + "jmx_exporter": [ + { + "enabled_in_broker": false + } + ], + "node_exporter": [ + { + "enabled_in_broker": false + } + ] + } + ] + } + ], + "storage_mode": "LOCAL", + "tags": { + "Environment": "test", + "Name": "c7n-test-msk-da399252" + }, + "tags_all": { + "Environment": "test", + "Name": "c7n-test-msk-da399252" + }, + "timeouts": null, + "zookeeper_connect_string": "z-1.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2181,z-2.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2181,z-3.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2181", + "zookeeper_connect_string_tls": "z-1.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2182,z-2.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2182,z-3.c7ntestda399252.yi695k.c18.kafka.us-east-1.amazonaws.com:2182" + } + }, + "aws_security_group": { + "msk": { + "arn": "arn:aws:ec2:us-east-1:644160558196:security-group/sg-049dfc123dab473e1", + "description": "Managed by Terraform", + "egress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 0, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "-1", + "security_groups": [], + "self": false, + "to_port": 0 + } + ], + "id": "sg-049dfc123dab473e1", + "ingress": [ + { + "cidr_blocks": [ + "10.0.0.0/16" + ], + "description": "", + "from_port": 9092, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 9098 + } + ], + "name": "msk-test-sg-20251216211048363700000001", + "name_prefix": "msk-test-sg-", + "owner_id": "644160558196", + "revoke_rules_on_delete": false, + "tags": { + "Name": "msk-test-sg-da399252" + }, + "tags_all": { + "Name": "msk-test-sg-da399252" + }, + "timeouts": null, + "vpc_id": "vpc-06cc97351657fe6e1" + } + }, + "aws_subnet": { + "msk": { + "arn": "arn:aws:ec2:us-east-1:644160558196:subnet/subnet-02e879cee93508233", + "assign_ipv6_address_on_creation": false, + "availability_zone": "us-east-1a", + "availability_zone_id": "use1-az6", + "cidr_block": "10.0.0.0/24", + "customer_owned_ipv4_pool": "", + "enable_dns64": false, + "enable_lni_at_device_index": 0, + "enable_resource_name_dns_a_record_on_launch": false, + "enable_resource_name_dns_aaaa_record_on_launch": false, + "id": "subnet-02e879cee93508233", + "ipv6_cidr_block": "", + "ipv6_cidr_block_association_id": "", + "ipv6_native": false, + "map_customer_owned_ip_on_launch": false, + "map_public_ip_on_launch": false, + "outpost_arn": "", + "owner_id": "644160558196", + "private_dns_hostname_type_on_launch": "ip-name", + "tags": { + "Name": "msk-test-subnet-0-da399252" + }, + "tags_all": { + "Name": "msk-test-subnet-0-da399252" + }, + "timeouts": null, + "vpc_id": "vpc-06cc97351657fe6e1" + } + }, + "aws_vpc": { + "msk": { + "arn": "arn:aws:ec2:us-east-1:644160558196:vpc/vpc-06cc97351657fe6e1", + "assign_generated_ipv6_cidr_block": false, + "cidr_block": "10.0.0.0/16", + "default_network_acl_id": "acl-05e373e8daa5ad2b3", + "default_route_table_id": "rtb-099e9611774075b77", + "default_security_group_id": "sg-0538a9f3ca8bcf638", + "dhcp_options_id": "dopt-09806453be8480159", + "enable_dns_hostnames": true, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "id": "vpc-06cc97351657fe6e1", + "instance_tenancy": "default", + "ipv4_ipam_pool_id": null, + "ipv4_netmask_length": null, + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_network_border_group": "", + "ipv6_ipam_pool_id": "", + "ipv6_netmask_length": 0, + "main_route_table_id": "rtb-099e9611774075b77", + "owner_id": "644160558196", + "tags": { + "Name": "msk-test-vpc-da399252" + }, + "tags_all": { + "Name": "msk-test-vpc-da399252" + } + } + }, + "random_id": { + "id": { + "b64_std": "2jmSUg==", + "b64_url": "2jmSUg", + "byte_length": 4, + "dec": "3661206098", + "hex": "da399252", + "id": "2jmSUg", + "keepers": null, + "prefix": null + } + } + } +} \ No newline at end of file diff --git a/tests/test_kafka.py b/tests/test_kafka.py index 08f45e3f26e..da25abf6a7e 100644 --- a/tests/test_kafka.py +++ b/tests/test_kafka.py @@ -318,3 +318,45 @@ def test_configuration_delete(self): "configuration/foo-config" "72932df0-5080-40d1-b801-49fa8580952b-7" in cluster_configurations.get('Configurations')) + + +class TestKafkaBroker(BaseTest): + """Tests for kafka-broker resource using recorded placebo fixtures.""" + + def test_kafka_broker_query(self): + """Test basic broker enumeration from provisioned cluster.""" + factory = self.replay_flight_data('test_kafka_broker_query') + + p = self.load_policy({ + 'name': 'kafka-brokers', + 'resource': 'aws.kafka-broker', + }, session_factory=factory) + + resources = p.run() + self.assertGreaterEqual(len(resources), 2) + + for r in resources: + self.assertIn('BrokerId', r) + self.assertIn('c7n:parent-id', r) + self.assertIn('ClusterName', r) + self.assertIn('ClusterArn', r) + + def test_kafka_broker_metrics(self): + """Test metrics filter on kafka brokers.""" + factory = self.replay_flight_data('test_kafka_broker_metrics') + + p = self.load_policy({ + 'name': 'underutilized-kafka-brokers', + 'resource': 'aws.kafka-broker', + 'filters': [ + {'type': 'metrics', + 'name': 'CpuUser', + 'days': 14, + 'value': 10, + 'op': 'less-than'} + ] + }, session_factory=factory) + + resources = p.run() + self.assertEqual(len(resources), 2) + self.assertIn('c7n.metrics', resources[0]) diff --git a/tests/test_policy.py b/tests/test_policy.py index 278baa9b092..3d70e567c88 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -993,7 +993,8 @@ def test_resource_arn_info(self): 'healthcheck', 'event-rule-target', 'log-metric', 'support-case', 'transit-attachment', 'config-recorder', 'apigw-domain-name', 'backup-job', 'quicksight-account', 'codedeploy-config', - 'cleanrooms-collaboration-member', 's3files-mount-target'} + 'cleanrooms-collaboration-member', 's3files-mount-target', + 'kafka-broker'} missing_method = [] for k, v in manager.resources.items(): From 5ce2e421ad3841b57d02de202abe14056d134199 Mon Sep 17 00:00:00 2001 From: KodyMullins <117883415+KodyMullins@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:02:24 -0500 Subject: [PATCH 42/65] gcp - log-project-sink - add sink-overlap filter (#10673) --------- Co-authored-by: Daniel Lindsley Co-authored-by: Claude Opus 5.5 --- tools/c7n_gcp/c7n_gcp/resources/logging.py | 184 ++++- tools/c7n_gcp/c7n_gcp/utils.py | 681 ++++++++++++++++++ ...t-v2-projects-cloud-custodian-sinks_1.json | 104 +++ ...t-v2-projects-cloud-custodian-sinks_1.json | 120 +++ .../terraform/log_project_sink_exact/main.tf | 65 ++ .../log_project_sink_exact/tf_resources.json | 158 ++++ .../log_project_sink_overlap/main.tf | 78 ++ .../tf_resources.json | 180 +++++ tools/c7n_gcp/tests/test_gcp_utils.py | 465 ++++++++++++ tools/c7n_gcp/tests/test_logging.py | 195 +++++ 10 files changed, 2229 insertions(+), 1 deletion(-) create mode 100644 tools/c7n_gcp/tests/data/flights/log-project-sink-exact/get-v2-projects-cloud-custodian-sinks_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/log-project-sink-overlap/get-v2-projects-cloud-custodian-sinks_1.json create mode 100644 tools/c7n_gcp/tests/terraform/log_project_sink_exact/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/log_project_sink_exact/tf_resources.json create mode 100644 tools/c7n_gcp/tests/terraform/log_project_sink_overlap/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/log_project_sink_overlap/tf_resources.json create mode 100644 tools/c7n_gcp/tests/test_gcp_utils.py diff --git a/tools/c7n_gcp/c7n_gcp/resources/logging.py b/tools/c7n_gcp/c7n_gcp/resources/logging.py index f4767cc1cfe..cd05105cf09 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/logging.py +++ b/tools/c7n_gcp/c7n_gcp/resources/logging.py @@ -1,11 +1,20 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from collections import defaultdict + from c7n.utils import local_session, type_schema -from c7n.filters.core import ValueFilter +from c7n.filters.core import Filter, ValueFilter from c7n_gcp.actions import MethodAction from c7n_gcp.provider import resources from c7n_gcp.query import QueryResourceManager, TypeInfo +from c7n_gcp.utils import ( + CloudLoggingOverlap, + canonicalize_cloud_logging_filter, + compare_cloud_logging_filters, + normalize_cloud_logging_filter_spacing, + parse_cloud_logging_filter, +) # TODO .. folder, billing account, org sink # how to map them given a project level root entity sans use of c7n-org @@ -85,6 +94,179 @@ def __call__(self, sink): return super().__call__(sink[self.cache_key]) +@LogProjectSink.filter_registry.register('sink-overlap') +class LogProjectSinkOverlapFilter(Filter): + """Match enabled sinks whose effective routing filter is identical to, or + overlaps with, at least one other enabled sink targeting the same + destination. + + A sink's effective routing is its filter minus its enabled exclusions. + ``match`` controls how effective routing is compared: + + - ``overlap`` (default): two sinks overlap when some log entry could match + both. The analyser understands AND/OR/NOT, equality, inequality, and + numeric/severity ranges. When a filter also uses comparisons it cannot + evaluate, such as ``:`` and regular expression matches, global search + terms or ``LOG_ID``, sinks that are not provably disjoint are reported + as possible matches. + - ``exact``: effective routing is canonicalized before comparison so that + logically equivalent expressions (e.g. different field orderings) are + treated as duplicates. + + Disabled sinks are ignored. A sink whose effective routing cannot be + parsed is logged and left out of ``overlap`` comparisons; with ``exact`` + it only matches sinks with the same filter text. Each matched sink is + annotated with the names of the sinks it directly matches under + ``c7n:matched-sinks``, and with the names of the sinks it possibly matches + under ``c7n:possible-matched-sinks``. + + Sinks are only compared with the other sinks that reach this filter, so + filters placed before it narrow the comparison. To compare against every + sink in the project and then narrow the results, put ``sink-overlap`` + first. + + :example: + + Find all project sinks whose log routing overlaps, or possibly overlaps, + with a sibling sink: + + .. code-block:: yaml + + policies: + - name: gcp-log-project-sink-overlapping + resource: gcp.log-project-sink + filters: + - type: sink-overlap + + :example: + + Find only project sinks whose overlap with a sibling sink is proven: + + .. code-block:: yaml + + policies: + - name: gcp-log-project-sink-proven-overlap + resource: gcp.log-project-sink + filters: + - type: sink-overlap + - type: value + key: '"c7n:matched-sinks"' + value: not-null + + :example: + + Find all project sinks that are exact duplicates of another sink: + + .. code-block:: yaml + + policies: + - name: gcp-log-project-sink-exact-duplicates + resource: gcp.log-project-sink + filters: + - type: sink-overlap + match: exact + + """ + + schema = type_schema('sink-overlap', match={'enum': ['exact', 'overlap']}) + annotation_key = 'c7n:matched-sinks' + possible_annotation_key = 'c7n:possible-matched-sinks' + permissions = ('logging.sinks.list',) + + def _effective_filter(self, sink, exact): + # A sink routes entries that match its filter and none of its + # enabled exclusions. + exclusions = [ + exclusion['filter'] + for exclusion in sink.get('exclusions', ()) + if not exclusion.get('disabled', False) and exclusion.get('filter') + ] + try: + # Each filter is canonicalized on its own first, so unbalanced + # parentheses can't change how the filters combine. A filter that + # is empty once canonicalized, e.g. only a comment, is treated + # like a missing one. + parts = [] + routed = canonicalize_cloud_logging_filter(sink.get('filter')) + if routed: + parts.append(f'({routed})') + for exclusion in exclusions: + excluded = canonicalize_cloud_logging_filter(exclusion) + if excluded: + parts.append(f'NOT ({excluded})') + key = canonicalize_cloud_logging_filter(' AND '.join(parts)) + # Overlap comparison parses the key again. + parse_cloud_logging_filter(key) + return key + except ValueError as e: + self.log.warning( + f"Unable to parse effective filter of log sink {sink.get('name')}: {e}") + if not exact: + return None + # Identical unparseable filters are still exact duplicates. A + # canonical key always parses, so this cannot collide with one. + # Each filter is normalized on its own so a comment can't swallow + # the ones after it. + raw = [normalize_cloud_logging_filter_spacing(sink.get('filter'))] + [ + f'NOT ({normalize_cloud_logging_filter_spacing(e)})' for e in exclusions + ] + return ' AND '.join(raw) + + def process(self, resources, event=None): + exact = self.data.get('match', 'overlap') == 'exact' + destination_groups = defaultdict(lambda: defaultdict(list)) + + for sink in resources: + if sink.get('disabled', False): + continue + expression = self._effective_filter(sink, exact) + if expression is None: + continue + destination_groups[sink.get('destination')][expression].append(sink) + + matched = [] + for relation_groups in destination_groups.values(): + for sink, peers, possible in self._iter_matches(relation_groups, exact): + sink[self.annotation_key] = [p.get('name') for p in peers] + sink[self.possible_annotation_key] = [p.get('name') for p in possible] + matched.append(sink) + return matched + + def _compare(self, left, right, cache): + # Overlap is symmetric, so each pair of filters is compared once. + pair = (left, right) if left <= right else (right, left) + if pair not in cache: + result = compare_cloud_logging_filters(*pair) + # Identical effective filters route the same entries, so they + # overlap unless the filter provably matches nothing. + if left == right and result is CloudLoggingOverlap.POSSIBLE: + result = CloudLoggingOverlap.OVERLAP + cache[pair] = result + return cache[pair] + + def _iter_matches(self, relation_groups, exact): + # Each sink is paired with the sinks it directly matches, not with + # everything reachable through a chain of overlaps. + cache = {} + for expression, sinks in relation_groups.items(): + if exact: + results = [(sinks, CloudLoggingOverlap.OVERLAP)] + else: + results = [ + (peers, self._compare(expression, other, cache)) + for other, peers in relation_groups.items() + ] + for sink in sinks: + peers, possible = [], [] + for others, result in results: + if result is CloudLoggingOverlap.OVERLAP: + peers.extend(p for p in others if p is not sink) + elif result is CloudLoggingOverlap.POSSIBLE: + possible.extend(p for p in others if p is not sink) + if peers or possible: + yield sink, peers, possible + + @LogProjectSink.action_registry.register('delete') class DeletePubSubTopic(MethodAction): diff --git a/tools/c7n_gcp/c7n_gcp/utils.py b/tools/c7n_gcp/c7n_gcp/utils.py index 90ea70b9994..092d7ebde05 100644 --- a/tools/c7n_gcp/c7n_gcp/utils.py +++ b/tools/c7n_gcp/c7n_gcp/utils.py @@ -1,3 +1,684 @@ +import functools +import logging +import math +import re +from dataclasses import dataclass +from enum import Enum + + +log = logging.getLogger('c7n_gcp.utils') + + +_REGEX_OPERATORS = {'=~', '!~'} +_SEVERITY_ORDER = { + "default": 0, + "debug": 100, + "info": 200, + "notice": 300, + "warning": 400, + "error": 500, + "critical": 600, + "alert": 700, + "emergency": 800, +} + + +_STRING_PATTERN = r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'' +# A word may contain quoted segments, e.g. the field path jsonPayload."my key". +_WORD_PATTERN = rf'[^\s()=><:!~"\'](?:[^\s()=><:!~"\']|{_STRING_PATTERN})*' +# Regex tokenizer for Cloud Logging filter expressions. +# Two-character operators must appear before their single-character prefixes so +# that alternation priority resolves them correctly (e.g. ">=" before ">"). +_TOKEN_RE = re.compile( + rf'(?P{_STRING_PATTERN})' + r'|(?P--[^\n]*)' # "--" starts a comment that runs to the end of the line + r'|(?P\()' + r'|(?P\))' + r'|(?P>=|<=|!=|=~|!~|[=><:])' + rf'|(?P{_WORD_PATTERN}(?=\())' # a function name directly followed by "(" + rf'|(?P{_WORD_PATTERN})' + r'|(?P[^\s])' # catch-all: tolerate uncommon punctuation (but not whitespace) +) +# Logical operators are case-sensitive: lowercase and/or/not are search terms. +_LOGICAL_WORDS = {'AND', 'OR', 'NOT'} +_NEGATIVE_NUMBER_RE = re.compile(r'-\d') +# One segment of a field path, which may contain quoted parts. +_FIELD_SEGMENT_RE = re.compile(rf'(?:{_STRING_PATTERN}|[^."\'])+') +# LogEntry fields whose sub-fields are map or struct keys. +_KEYED_FIELDS = {'labels', 'jsonpayload', 'protopayload'} +_ESCAPED_QUOTE_RE = re.compile(r'\\(["\'\\])') + + +def _tokenize_cloud_logging_filter(expression): + tokens = [] + for m in _TOKEN_RE.finditer(expression): + kind, value = m.lastgroup, m.group() + if kind == 'COMMENT': + continue + if kind in ('WORD', 'FUNC') and value in _LOGICAL_WORDS: + tokens.append(('LOGIC', value)) + elif ( + kind in ('WORD', 'FUNC') + and value.startswith('-') + and not (tokens and tokens[-1][0] == 'OP') + and not _NEGATIVE_NUMBER_RE.match(value) + ): + # A leading minus on a term is shorthand for NOT, e.g. + # -resource.type="gce_instance". A minus after a comparison + # operator or before a digit is part of the value. + tokens.append(('LOGIC', 'NOT')) + if kind == 'FUNC' and value[1:]: + tokens.append(('FUNC', value[1:])) + elif kind == 'WORD': + # The rest may be a quoted string, e.g. -"foo bar". + tokens.extend(_tokenize_cloud_logging_filter(value[1:])) + elif kind == 'UNKNOWN': + tokens.append(('WORD', value)) # tolerate uncommon punctuation + else: + tokens.append((kind, value)) + return tokens + + +class _TokenStream: + def __init__(self, tokens): + self.tokens = tokens + self.index = 0 + + def peek(self): + if self.index >= len(self.tokens): + return None + return self.tokens[self.index] + + def consume(self): + token = self.peek() + if token is not None: + self.index += 1 + return token + + +class CloudLoggingFilterParser: + """Parser for Cloud Logging query expressions. + + The parser supports logical operators (AND/OR/NOT and the ``-`` shorthand + for NOT), implicit AND between adjacent terms, grouping parentheses, + parenthesized value lists on the right-hand side of a comparison, and all + documented comparison/match operators used in sink filters. + + Cloud Logging gives NOT the highest precedence, followed by OR and then + AND, so ``a OR b AND c`` means ``(a OR b) AND c``. + """ + + @classmethod + def parse(cls, expression): + tokens = _tokenize_cloud_logging_filter(expression or "") + if not tokens: + # An empty filter matches every log entry. + return cls._build_result({"type": "all"}) + + stream = _TokenStream(tokens) + ast = cls._parse_and(stream, cls._parse_predicate) + if stream.peek() is not None: + raise ValueError("Unable to parse full Cloud Logging filter expression") + return cls._build_result(ast) + + @classmethod + def canonicalize(cls, expression): + parsed = cls.parse(expression) + return cls._canonicalize_node(parsed["ast"]) + + # The boolean grammar is shared by whole expressions and by parenthesized + # value lists, so each level takes the ``leaf`` parser to use for its + # innermost operands. + + @classmethod + def _parse_and(cls, stream, leaf): + # AND has the lowest precedence. Adjacent terms with no operator + # between them are an implicit AND. + children = [cls._parse_or(stream, leaf)] + while True: + token = stream.peek() + if token is None or token[0] == "RPAREN": + break + if token == ("LOGIC", "AND"): + stream.consume() + children.append(cls._parse_or(stream, leaf)) + return cls._join("and", children) + + @classmethod + def _parse_or(cls, stream, leaf): + children = [cls._parse_not(stream, leaf)] + while stream.peek() == ("LOGIC", "OR"): + stream.consume() + children.append(cls._parse_not(stream, leaf)) + return cls._join("or", children) + + @classmethod + def _parse_not(cls, stream, leaf): + if stream.peek() == ("LOGIC", "NOT"): + stream.consume() + return {"type": "not", "expr": cls._parse_not(stream, leaf)} + return cls._parse_primary(stream, leaf) + + @classmethod + def _parse_primary(cls, stream, leaf): + token = stream.peek() + if token is None: + raise ValueError("Unexpected end of Cloud Logging filter expression") + + if token[0] == "LPAREN": + stream.consume() + node = cls._parse_and(stream, leaf) + cls._expect_closing_paren(stream) + return node + return leaf(stream) + + @staticmethod + def _join(node_type, children): + # Flatten nested nodes of the same type so that equivalent + # expressions with different grouping share one shape. + flat = [] + for child in children: + if child["type"] == node_type: + flat.extend(child["children"]) + else: + flat.append(child) + if len(flat) == 1: + return flat[0] + return {"type": node_type, "children": flat} + + @staticmethod + def _expect_closing_paren(stream): + closing = stream.consume() + if not closing or closing[0] != "RPAREN": + raise ValueError("Missing closing parenthesis in filter expression") + + @classmethod + def _parse_predicate(cls, stream): + lhs = cls._parse_operand(stream) + token = stream.peek() + if token is None or token[0] != "OP": + return {"type": "term", "tokens": lhs} + op = stream.consume()[1] + + token = stream.peek() + if token is None or token[0] not in ("WORD", "STRING", "LPAREN"): + raise ValueError("Malformed comparison in Cloud Logging filter expression") + if token[0] == "LPAREN": + # The field and operator apply to each element of a value list, + # e.g. resource.type=("gce_instance" OR "k8s_container"). + stream.consume() + node = cls._parse_and(stream, lambda s: cls._parse_value(s, lhs, op)) + cls._expect_closing_paren(stream) + return node + return cls._parse_value(stream, lhs, op) + + @classmethod + def _parse_operand(cls, stream): + # A field path, value or search term. A name directly followed by a + # parenthesized argument list is a function call such as + # sample(insertId, 0.1) or LOG_ID("..."). + token = stream.peek() + if token is None or token[0] not in ("WORD", "STRING", "FUNC"): + raise ValueError("Empty predicate in Cloud Logging filter expression") + tokens = [stream.consume()] + if token[0] == "FUNC": + depth = 0 + while True: + token = stream.consume() + if token is None: + raise ValueError("Missing closing parenthesis in filter expression") + tokens.append(token) + if token[0] == "LPAREN": + depth += 1 + elif token[0] == "RPAREN": + depth -= 1 + if depth == 0: + break + return tokens + + @staticmethod + def _parse_value(stream, lhs, op): + token = stream.peek() + if token is None or token[0] not in ("WORD", "STRING"): + raise ValueError("Malformed comparison in Cloud Logging filter expression") + stream.consume() + return {"type": "predicate", "lhs_tokens": lhs, "op": op, "rhs_tokens": [token]} + + @classmethod + def _build_result(cls, ast): + logical_operators = set() + comparison_operators = set() + predicates = [] + terms = [] + + def visit(node): + node_type = node["type"] + if node_type in ("and", "or"): + logical_operators.add(node_type) + for child in node["children"]: + visit(child) + elif node_type == "not": + logical_operators.add("not") + visit(node["expr"]) + elif node_type == "predicate": + comparison_operators.add(node["op"]) + predicates.append( + { + "lhs": cls._render_tokens(node["lhs_tokens"]), + "op": node["op"], + "rhs": cls._render_tokens(node["rhs_tokens"]), + } + ) + elif node_type == "term": + terms.append(cls._render_tokens(node["tokens"])) + + visit(ast) + return { + "ast": ast, + "logical_operators": sorted(logical_operators), + "comparison_operators": sorted(comparison_operators), + "predicates": predicates, + "terms": terms, + } + + @classmethod + def _canonicalize_node(cls, node): + node_type = node["type"] + # The canonical form must re-parse to the same expression, so logical + # operators stay uppercase. + if node_type in ("and", "or"): + children = sorted({cls._canonicalize_node(c) for c in node["children"]}) + if len(children) == 1: + return children[0] + joiner = " AND " if node_type == "and" else " OR " + return "(" + joiner.join(children) + ")" + if node_type == "not": + return f"NOT {cls._canonicalize_node(node['expr'])}" + if node_type == "all": + return "" + # Comparisons ignore letter case except for regular expressions. + if node_type == "predicate": + lhs = cls._canonicalize_field(node["lhs_tokens"]) + rhs = cls._canonicalize_value( + node["rhs_tokens"][0], lower=node["op"] not in _REGEX_OPERATORS + ) + return f"{lhs} {node['op']} {rhs}" + return cls._render_tokens(node["tokens"], lower=True) + + @classmethod + def _canonicalize_field(cls, tokens): + # Built-in LogEntry field names ignore letter case, but map and struct + # keys are case-sensitive, so only the path up to the first keyed + # field is lowercased, e.g. resource.labels.Zone keeps "Zone". + if len(tokens) != 1 or tokens[0][0] != "WORD": + return cls._render_tokens(tokens) + path = tokens[0][1] + end = 0 + for m in _FIELD_SEGMENT_RE.finditer(path): + end = m.end() + if m.group().lower() in _KEYED_FIELDS: + break + return path[:end].lower() + path[end:] + + @staticmethod + def _canonicalize_value(token, lower): + # A value means the same quoted or not, so render it double-quoted + # where that doesn't need new escapes. + token_type, value = token + if token_type == "STRING" and value[0] == "'" and '"' not in value: + value = f'"{value[1:-1]}"' + elif token_type == "WORD" and not set(value) & set("\"'\\"): + value = f'"{value}"' + return value.lower() if lower else value + + @classmethod + def _render_tokens(cls, tokens, lower=False): + rendered = "" + previous = None + for token_type, value in tokens: + if lower and token_type in ("WORD", "STRING", "FUNC"): + value = value.lower() + # A function name must stay directly followed by its "(". + if rendered and previous not in ("FUNC", "LPAREN") and token_type != "RPAREN": + rendered += " " + rendered += value + previous = token_type + return rendered + + +def parse_cloud_logging_filter(expression): + """Parse a Cloud Logging filter expression into parts and an AST. + + Returns a dict with ``ast``, ``logical_operators`` (``and``/``or``/ + ``not``), ``comparison_operators`` (e.g. ``=``, ``>=``, ``:``), + ``predicates`` (``lhs``/``op``/``rhs`` dicts) and ``terms`` (bare search + terms and function calls). + """ + return CloudLoggingFilterParser.parse(expression) + + +def canonicalize_cloud_logging_filter(expression): + """Return a canonical string representation of a Cloud Logging filter.""" + return CloudLoggingFilterParser.canonicalize(expression) + + +def normalize_cloud_logging_filter_spacing(expression): + """Return a Cloud Logging filter with comments dropped and whitespace + normalized outside quoted strings. + + Unlike canonicalization this doesn't parse the filter, so it also works + on filters that don't parse. + """ + tokens = _tokenize_cloud_logging_filter(expression or "") + return CloudLoggingFilterParser._render_tokens(tokens) + + +class CloudLoggingOverlap(Enum): + """Result of comparing two Cloud Logging filters.""" + + # No log entry can match both filters. + DISJOINT = 'disjoint' + # The filters are not provably disjoint, but they contain comparisons the + # analyser cannot evaluate, so overlap could not be proven either. + POSSIBLE = 'possible' + # Some log entry matches both filters. + OVERLAP = 'overlap' + + +def compare_cloud_logging_filters(left_expression, right_expression): + """Compare two Cloud Logging filters for overlap. + + Each filter is rewritten as an OR of AND-ed literals (disjunctive normal + form), and every conjunction of one filter is combined with every + conjunction of the other. + + A combined conjunction is provably unsatisfiable when it contains: + + - a literal and its negation, e.g. ``LOG_ID("x")`` and ``NOT LOG_ID("x")``; + - two different equality values for one field, or an equality value that + is also excluded with ``!=`` or ``NOT``; + - numeric or severity ranges with no common value, including a range that + is empty on its own, e.g. ``severity>=ERROR AND severity<=WARNING``. + + It is provably satisfiable when, in addition, every literal is an + equality, inequality or numeric/severity range comparison. Literals the + analyser cannot evaluate, such as ``:`` and regular expression matches, + global search terms and functions like ``LOG_ID``, leave the result + ``POSSIBLE`` unless a contradiction is found. Filters whose normal form + grows beyond ``_MAX_DNF_CONJUNCTIONS`` are logged and also ``POSSIBLE``. + + ``jsonPayload`` and ``protoPayload`` fields may hold arrays, and a + comparison on an array matches when any element matches, so conflicting + comparisons on those fields give ``POSSIBLE`` rather than ``DISJOINT``. + """ + left = _cloud_logging_filter_dnf(left_expression) + right = _cloud_logging_filter_dnf(right_expression) + if left is None or right is None: + return CloudLoggingOverlap.POSSIBLE + + result = CloudLoggingOverlap.DISJOINT + for left_conj in left: + for right_conj in right: + conj_result = _cloud_logging_conjunction_satisfiable(left_conj + right_conj) + if conj_result is CloudLoggingOverlap.OVERLAP: + return conj_result + if conj_result is CloudLoggingOverlap.POSSIBLE: + result = conj_result + return result + + +_MAX_DNF_CONJUNCTIONS = 256 + + +@functools.lru_cache(maxsize=1024) +def _cloud_logging_filter_dnf(expression): + dnf = _to_cloud_logging_dnf(parse_cloud_logging_filter(expression)['ast']) + if dnf is None: + log.warning( + "Cloud Logging filter is too complex to compare for overlap " + f"(more than {_MAX_DNF_CONJUNCTIONS} conjunctions): {expression}") + return dnf + + +def _to_cloud_logging_dnf(node, negated=False): + """Return ``node`` as a list of conjunctions of ``(negated, leaf)`` literals. + + NOT is pushed down to the leaves with De Morgan's laws. Returns None when + the result would exceed ``_MAX_DNF_CONJUNCTIONS`` conjunctions. + """ + node_type = node['type'] + if node_type == 'not': + return _to_cloud_logging_dnf(node['expr'], not negated) + if node_type == 'all': + # One empty conjunction is always true; no conjunctions is never true. + return [] if negated else [[]] + if node_type not in ('and', 'or'): + return [[(negated, node)]] + + parts = [_to_cloud_logging_dnf(child, negated) for child in node['children']] + if any(part is None for part in parts): + return None + # A negated AND is an OR of the negated children, and vice versa. + if (node_type == 'and') == negated: + result = [conj for part in parts for conj in part] + else: + result = [[]] + for part in parts: + result = [conj + other for conj in result for other in part] + if len(result) > _MAX_DNF_CONJUNCTIONS: + return None + if len(result) > _MAX_DNF_CONJUNCTIONS: + return None + return result + + +def _cloud_logging_conjunction_satisfiable(literals): + """Return OVERLAP if the conjunction is provably satisfiable, DISJOINT if + it is provably unsatisfiable, and POSSIBLE otherwise.""" + polarity = {} + fields = {} + result = CloudLoggingOverlap.OVERLAP + for negated, node in literals: + key = CloudLoggingFilterParser._canonicalize_node(node) + if polarity.setdefault(key, negated) != negated: + return CloudLoggingOverlap.DISJOINT + if node['type'] == 'term': + result = CloudLoggingOverlap.POSSIBLE + continue + field = CloudLoggingFilterParser._canonicalize_field(node['lhs_tokens']) + value = _normalize_cloud_logging_rhs( + CloudLoggingFilterParser._render_tokens(node['rhs_tokens']) + ) + fields.setdefault(field, []).append((negated, node['op'], value)) + + # Built-in field names are already lowercased, so paths that still + # differ only in case differ in a map or struct key. Don't rely on how + # Cloud Logging matches the case of those keys. + if len({field.lower() for field in fields}) < len(fields): + result = CloudLoggingOverlap.POSSIBLE + + for field, constraints in fields.items(): + field_result = _cloud_logging_field_satisfiable(field, constraints) + if field_result is CloudLoggingOverlap.DISJOINT: + return field_result + if field_result is CloudLoggingOverlap.POSSIBLE: + result = field_result + return result + + +# Comparisons that can only match when the field is present on the entry. +_PRESENCE_OPERATORS = {'=', '>', '>=', '<', '<='} +_ORDERED_OPERATORS = {'>', '>=', '<', '<='} +_COMPLEMENT_OPERATORS = {'=': '!=', '!=': '=', '>': '<=', '>=': '<', '<': '>=', '<=': '>'} + + +def _cloud_logging_field_satisfiable(field, constraints): + result = _cloud_logging_single_value_satisfiable(field, constraints) + # A payload field may be an array, and a comparison on an array matches + # when any element matches, so one entry can satisfy x="a" AND x="b". + # A single-element array behaves like a single value, so proven overlap + # still holds. + if result is CloudLoggingOverlap.DISJOINT and field.lower().startswith( + _REPEATABLE_FIELD_PREFIXES + ): + return CloudLoggingOverlap.POSSIBLE + return result + + +_REPEATABLE_FIELD_PREFIXES = ('jsonpayload.', 'protopayload.') + + +def _cloud_logging_single_value_satisfiable(field, constraints): + # Built-in LogEntry field names ignore letter case. + severity = field.lower() == 'severity' + # Every entry has a severity: LogEntry.severity defaults to DEFAULT. + present = severity or any( + not negated and op in _PRESENCE_OPERATORS for negated, op, _ in constraints + ) + understood = True + effective = [] + for negated, op, value in constraints: + if negated: + if op == '=': + # NOT field="x" holds when the field is absent or has another + # value, so it always excludes "x". + op = '!=' + elif present and op in _COMPLEMENT_OPERATORS: + # Once another literal requires the field to be present, a + # negated comparison is its complement. + op = _COMPLEMENT_OPERATORS[op] + else: + # May hold only because the field is absent. + understood = False + continue + if op in _ORDERED_OPERATORS: + if _coerce_cloud_logging_ordered_value(value, severity) is None: + understood = False + continue + elif op not in ('=', '!='): + understood = False + continue + effective.append((op, value)) + + def value_key(value): + # Numbers compare by value, e.g. 5 and 5.0, or ERROR and 500 for severity. + number = _coerce_cloud_logging_ordered_value(value, severity) + return value if number is None else number + + eq = {value_key(value) for op, value in effective if op == '='} + ne = {value_key(value) for op, value in effective if op == '!='} + if len(eq) > 1 or eq & ne: + return CloudLoggingOverlap.DISJOINT + + interval = _to_cloud_logging_numeric_interval(effective, severity) + if interval is None and severity: + # Severity is always one of the fixed levels, even unconstrained. + interval = _Interval() + if interval is not None: + excluded = {value for value in ne if isinstance(value, float)} + if severity: + # Every entry has exactly one of the fixed severity levels. + if not any( + interval.contains(level) and level not in excluded + for level in _SEVERITY_ORDER.values() + ): + return CloudLoggingOverlap.DISJOINT + else: + if interval.empty: + return CloudLoggingOverlap.DISJOINT + if interval.lower == interval.upper and interval.lower in excluded: + return CloudLoggingOverlap.DISJOINT + if any(interval.contains(v) for v in excluded): + # Excluding values inside a range only leaves a match if the + # field is not restricted to those values, e.g. integers. + understood = False + if any(not isinstance(v, float) for v in eq): + # A non-numeric equality alongside a numeric range. + understood = False + + return CloudLoggingOverlap.OVERLAP if understood else CloudLoggingOverlap.POSSIBLE + + +def _normalize_cloud_logging_rhs(rhs): + value = rhs.strip() + if (value.startswith('"') and value.endswith('"')) or ( + value.startswith("'") and value.endswith("'") + ): + # The same value can be written with different escapes, e.g. + # "a\"b" and 'a"b'. + value = _ESCAPED_QUOTE_RE.sub(r'\1', value[1:-1]) + return value.lower() + + +@dataclass +class _Interval: + """Closed/open numeric interval used for Cloud Logging filter overlap checks.""" + lower: float = float('-inf') + lower_inclusive: bool = True + upper: float = float('inf') + upper_inclusive: bool = True + + @property + def empty(self): + if self.lower > self.upper: + return True + return self.lower == self.upper and not (self.lower_inclusive and self.upper_inclusive) + + def contains(self, value): + above = value > self.lower or (value == self.lower and self.lower_inclusive) + below = value < self.upper or (value == self.upper and self.upper_inclusive) + return above and below + + +def _to_cloud_logging_numeric_interval(predicates, severity=False): + lower = float('-inf') + lower_inclusive = True + upper = float('inf') + upper_inclusive = True + has_numeric = False + + for op, value in predicates: + if op not in ('=', '>', '>=', '<', '<='): + continue + numeric_value = _coerce_cloud_logging_ordered_value(value, severity) + if numeric_value is None: + continue + + has_numeric = True + # Each comparison can only narrow the interval. At an equal bound an + # exclusive comparison is the narrower one, so an inclusive one never + # replaces it. + if op in ('=', '>', '>='): + inclusive = op != '>' + if numeric_value > lower or (numeric_value == lower and not inclusive): + lower, lower_inclusive = numeric_value, inclusive + if op in ('=', '<', '<='): + inclusive = op != '<' + if numeric_value < upper or (numeric_value == upper and not inclusive): + upper, upper_inclusive = numeric_value, inclusive + + if not has_numeric: + return None + return _Interval(lower, lower_inclusive, upper, upper_inclusive) + + +def _coerce_cloud_logging_ordered_value(value, severity=False): + try: + number = float(value) + except (TypeError, ValueError): + pass + else: + # float() also accepts "inf" and "nan", which are not numbers here. + if math.isfinite(number): + return number + + # Severity names only order the top-level severity field. + lowered = str(value).strip().lower() + if severity and lowered in _SEVERITY_ORDER: + return float(_SEVERITY_ORDER[lowered]) + return None + + def get_firewall_port_ranges(firewall_resources): for r_index, r in enumerate(firewall_resources): action = "allowed" if "allowed" in r else "denied" diff --git a/tools/c7n_gcp/tests/data/flights/log-project-sink-exact/get-v2-projects-cloud-custodian-sinks_1.json b/tools/c7n_gcp/tests/data/flights/log-project-sink-exact/get-v2-projects-cloud-custodian-sinks_1.json new file mode 100644 index 00000000000..abb0a690c23 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/log-project-sink-exact/get-v2-projects-cloud-custodian-sinks_1.json @@ -0,0 +1,104 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 02:50:19 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=123", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "4703", + "-content-encoding": "gzip", + "content-location": "https://logging.googleapis.com/v2/projects/cloud-custodian/sinks?alt=json" + }, + "body": { + "sinks": [ + { + "name": "c7n-sink-exact-a", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "createTime": "2026-10-14T02:50:14.935447113Z", + "updateTime": "2026-10-14T02:50:14.935447113Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-a" + }, + { + "name": "c7n-sink-exact-b", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "resource.type=\"gce_instance\" AND severity>=ERROR", + "createTime": "2026-10-14T02:50:15.048121686Z", + "updateTime": "2026-10-14T02:50:15.048121686Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-b" + }, + { + "name": "c7n-sink-exact-disabled", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "createTime": "2026-10-14T02:50:15.047932126Z", + "updateTime": "2026-10-14T02:50:15.047932126Z", + "disabled": true, + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-disabled" + }, + { + "name": "c7n-sink-exact-no-filter-a", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "createTime": "2026-10-14T02:50:15.041346065Z", + "updateTime": "2026-10-14T02:50:15.041346065Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-no-filter-a" + }, + { + "name": "c7n-sink-exact-no-filter-b", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "createTime": "2026-10-14T02:50:15.048104698Z", + "updateTime": "2026-10-14T02:50:15.048104698Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-no-filter-b" + }, + { + "name": "c7n-sink-exact-non-match", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=WARNING AND resource.type=\"gce_instance\"", + "createTime": "2026-10-14T02:50:15.041484620Z", + "updateTime": "2026-10-14T02:50:15.041484620Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-non-match" + }, + { + "name": "c7n-sink-exact-other-dest", + "destination": "pubsub.googleapis.com/projects/cloud-custodian/topics/c7n-sink-exact-other", + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "writerIdentity": "serviceAccount:service-12345678901@gcp-sa-logging.iam.gserviceaccount.com", + "createTime": "2026-10-14T02:50:18.202498047Z", + "updateTime": "2026-10-14T02:50:18.202498047Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-other-dest" + }, + { + "name": "c7n-sink-exact-with-exclusion", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "createTime": "2026-10-14T02:50:15.047930281Z", + "updateTime": "2026-10-14T02:50:15.047930281Z", + "exclusions": [ + { + "name": "zone", + "filter": "resource.labels.zone=\"us-east1-b\"" + } + ], + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-exact-with-exclusion" + }, + { + "name": "_Required", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Required", + "filter": "LOG_ID(\"cloudaudit.googleapis.com/activity\") OR LOG_ID(\"externalaudit.googleapis.com/activity\") OR LOG_ID(\"cloudaudit.googleapis.com/system_event\") OR LOG_ID(\"externalaudit.googleapis.com/system_event\") OR LOG_ID(\"cloudaudit.googleapis.com/access_transparency\") OR LOG_ID(\"externalaudit.googleapis.com/access_transparency\")", + "resourceName": "projects/cloud-custodian/sinks/_Required" + }, + { + "name": "_Default", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "NOT LOG_ID(\"cloudaudit.googleapis.com/activity\") AND NOT LOG_ID(\"externalaudit.googleapis.com/activity\") AND NOT LOG_ID(\"cloudaudit.googleapis.com/system_event\") AND NOT LOG_ID(\"externalaudit.googleapis.com/system_event\") AND NOT LOG_ID(\"cloudaudit.googleapis.com/access_transparency\") AND NOT LOG_ID(\"externalaudit.googleapis.com/access_transparency\")", + "resourceName": "projects/cloud-custodian/sinks/_Default" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/log-project-sink-overlap/get-v2-projects-cloud-custodian-sinks_1.json b/tools/c7n_gcp/tests/data/flights/log-project-sink-overlap/get-v2-projects-cloud-custodian-sinks_1.json new file mode 100644 index 00000000000..3e2bad611e7 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/log-project-sink-overlap/get-v2-projects-cloud-custodian-sinks_1.json @@ -0,0 +1,120 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 02:50:31 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=129", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "5160", + "-content-encoding": "gzip", + "content-location": "https://logging.googleapis.com/v2/projects/cloud-custodian/sinks?alt=json" + }, + "body": { + "sinks": [ + { + "name": "c7n-sink-overlap-base", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR", + "createTime": "2026-10-14T02:50:27.494737431Z", + "updateTime": "2026-10-14T02:50:27.494737431Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-base" + }, + { + "name": "c7n-sink-overlap-base-copy", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR", + "createTime": "2026-10-14T02:50:27.495304868Z", + "updateTime": "2026-10-14T02:50:27.495304868Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-base-copy" + }, + { + "name": "c7n-sink-overlap-disabled", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR", + "createTime": "2026-10-14T02:50:27.488386725Z", + "updateTime": "2026-10-14T02:50:27.488386725Z", + "disabled": true, + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-disabled" + }, + { + "name": "c7n-sink-overlap-excl-all", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR", + "createTime": "2026-10-14T02:50:27.488468249Z", + "updateTime": "2026-10-14T02:50:27.488468249Z", + "exclusions": [ + { + "name": "all", + "filter": "severity>=ERROR" + } + ], + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-excl-all" + }, + { + "name": "c7n-sink-overlap-excl-partial", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=ERROR", + "createTime": "2026-10-14T02:50:27.488694002Z", + "updateTime": "2026-10-14T02:50:27.488694002Z", + "exclusions": [ + { + "name": "gce", + "filter": "resource.type=\"gce_instance\"" + } + ], + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-excl-partial" + }, + { + "name": "c7n-sink-overlap-non-match", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity=ERROR AND NOT resource.type=\"gce_instance\"", + "createTime": "2026-10-14T02:50:27.494946027Z", + "updateTime": "2026-10-14T02:50:27.494946027Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-not-clause" + }, + { + "name": "c7n-sink-overlap-other-dest", + "destination": "pubsub.googleapis.com/projects/cloud-custodian/topics/c7n-sink-overlap-other", + "filter": "severity>=ERROR", + "writerIdentity": "serviceAccount:service-12345678901@gcp-sa-logging.iam.gserviceaccount.com", + "createTime": "2026-10-14T02:50:30.038994256Z", + "updateTime": "2026-10-14T02:50:30.038994256Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-other-dest" + }, + { + "name": "c7n-sink-overlap-subset", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "severity>=CRITICAL", + "createTime": "2026-10-14T02:50:27.381915096Z", + "updateTime": "2026-10-14T02:50:27.381915096Z", + "resourceName": "projects/cloud-custodian/sinks/c7n-sink-overlap-subset" + }, + { + "name": "_Required", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Required", + "filter": "LOG_ID(\"cloudaudit.googleapis.com/activity\") OR LOG_ID(\"externalaudit.googleapis.com/activity\") OR LOG_ID(\"cloudaudit.googleapis.com/system_event\") OR LOG_ID(\"externalaudit.googleapis.com/system_event\") OR LOG_ID(\"cloudaudit.googleapis.com/access_transparency\") OR LOG_ID(\"externalaudit.googleapis.com/access_transparency\")", + "resourceName": "projects/cloud-custodian/sinks/_Required" + }, + { + "name": "_Default", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "filter": "NOT LOG_ID(\"cloudaudit.googleapis.com/activity\") AND NOT LOG_ID(\"externalaudit.googleapis.com/activity\") AND NOT LOG_ID(\"cloudaudit.googleapis.com/system_event\") AND NOT LOG_ID(\"externalaudit.googleapis.com/system_event\") AND NOT LOG_ID(\"cloudaudit.googleapis.com/access_transparency\") AND NOT LOG_ID(\"externalaudit.googleapis.com/access_transparency\")", + "resourceName": "projects/cloud-custodian/sinks/_Default" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/log_project_sink_exact/main.tf b/tools/c7n_gcp/tests/terraform/log_project_sink_exact/main.tf new file mode 100644 index 00000000000..fae546b82a2 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/log_project_sink_exact/main.tf @@ -0,0 +1,65 @@ +variable "google_project_id" { + description = "GCP project ID, for sink destinations" +} + +provider "google" {} + +locals { + destination = "logging.googleapis.com/projects/${var.google_project_id}/locations/global/buckets/_Default" +} + +resource "google_pubsub_topic" "other" { + name = "c7n-sink-exact-other" +} + +resource "google_logging_project_sink" "a" { + name = "c7n-sink-exact-a" + destination = local.destination + filter = "severity>=ERROR AND resource.type=\"gce_instance\"" +} + +resource "google_logging_project_sink" "b" { + name = "c7n-sink-exact-b" + destination = local.destination + filter = "resource.type=\"gce_instance\" AND severity>=ERROR" +} + +resource "google_logging_project_sink" "non_match" { + name = "c7n-sink-exact-non-match" + destination = local.destination + filter = "severity>=WARNING AND resource.type=\"gce_instance\"" +} + +resource "google_logging_project_sink" "disabled" { + name = "c7n-sink-exact-disabled" + destination = local.destination + filter = "severity>=ERROR AND resource.type=\"gce_instance\"" + disabled = true +} + +resource "google_logging_project_sink" "with_exclusion" { + name = "c7n-sink-exact-with-exclusion" + destination = local.destination + filter = "severity>=ERROR AND resource.type=\"gce_instance\"" + + exclusions { + name = "zone" + filter = "resource.labels.zone=\"us-east1-b\"" + } +} + +resource "google_logging_project_sink" "other_destination" { + name = "c7n-sink-exact-other-dest" + destination = "pubsub.googleapis.com/${google_pubsub_topic.other.id}" + filter = "severity>=ERROR AND resource.type=\"gce_instance\"" +} + +resource "google_logging_project_sink" "no_filter_a" { + name = "c7n-sink-exact-no-filter-a" + destination = local.destination +} + +resource "google_logging_project_sink" "no_filter_b" { + name = "c7n-sink-exact-no-filter-b" + destination = local.destination +} diff --git a/tools/c7n_gcp/tests/terraform/log_project_sink_exact/tf_resources.json b/tools/c7n_gcp/tests/terraform/log_project_sink_exact/tf_resources.json new file mode 100644 index 00000000000..b95cea63a02 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/log_project_sink_exact/tf_resources.json @@ -0,0 +1,158 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_logging_project_sink": { + "a": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-a", + "name": "c7n-sink-exact-a", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "b": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "resource.type=\"gce_instance\" AND severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-b", + "name": "c7n-sink-exact-b", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "disabled": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": true, + "exclusions": [], + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-disabled", + "name": "c7n-sink-exact-disabled", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "no_filter_a": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-no-filter-a", + "name": "c7n-sink-exact-no-filter-a", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "no_filter_b": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-no-filter-b", + "name": "c7n-sink-exact-no-filter-b", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "non_match": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "severity>=WARNING AND resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-non-match", + "name": "c7n-sink-exact-non-match", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "other_destination": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "pubsub.googleapis.com/projects/cloud-custodian/topics/c7n-sink-exact-other", + "disabled": false, + "exclusions": [], + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-other-dest", + "name": "c7n-sink-exact-other-dest", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "serviceAccount:service-12345678901@gcp-sa-logging.iam.gserviceaccount.com" + }, + "with_exclusion": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [ + { + "description": "", + "disabled": false, + "filter": "resource.labels.zone=\"us-east1-b\"", + "name": "zone" + } + ], + "filter": "severity>=ERROR AND resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-exact-with-exclusion", + "name": "c7n-sink-exact-with-exclusion", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + } + }, + "google_pubsub_topic": { + "other": { + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "id": "projects/cloud-custodian/topics/c7n-sink-exact-other", + "ingestion_data_source_settings": [], + "kms_key_name": "", + "labels": null, + "message_retention_duration": "", + "message_storage_policy": [], + "message_transforms": [], + "name": "c7n-sink-exact-other", + "project": "cloud-custodian", + "schema_settings": [], + "tags": null, + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "timeouts": null + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/log_project_sink_overlap/main.tf b/tools/c7n_gcp/tests/terraform/log_project_sink_overlap/main.tf new file mode 100644 index 00000000000..f0f13d324b6 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/log_project_sink_overlap/main.tf @@ -0,0 +1,78 @@ +variable "google_project_id" { + description = "GCP project ID, for sink destinations" +} + +provider "google" {} + +locals { + destination = "logging.googleapis.com/projects/${var.google_project_id}/locations/global/buckets/_Default" +} + +resource "google_pubsub_topic" "other" { + name = "c7n-sink-overlap-other" +} + +resource "google_logging_project_sink" "base" { + name = "c7n-sink-overlap-base" + destination = local.destination + filter = "severity>=ERROR" +} + +resource "google_logging_project_sink" "base_copy" { + name = "c7n-sink-overlap-base-copy" + destination = local.destination + filter = "severity>=ERROR" +} + +resource "google_logging_project_sink" "subset" { + name = "c7n-sink-overlap-subset" + destination = local.destination + filter = "severity>=CRITICAL" +} + +resource "google_logging_project_sink" "not_clause" { + name = "c7n-sink-overlap-not-clause" + destination = local.destination + filter = "severity>=ERROR AND NOT resource.type=\"gce_instance\"" +} + +resource "google_logging_project_sink" "exclusion_partial" { + name = "c7n-sink-overlap-excl-partial" + destination = local.destination + filter = "severity>=ERROR" + + exclusions { + name = "gce" + filter = "resource.type=\"gce_instance\"" + } +} + +resource "google_logging_project_sink" "exclusion_all" { + name = "c7n-sink-overlap-excl-all" + destination = local.destination + filter = "severity>=ERROR" + + exclusions { + name = "all" + filter = "severity>=ERROR" + } +} + +resource "google_logging_project_sink" "non_match" { + name = "c7n-sink-overlap-non-match" + destination = local.destination + filter = "severity=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-base", + "name": "c7n-sink-overlap-base", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "base_copy": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-base-copy", + "name": "c7n-sink-overlap-base-copy", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "disabled": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": true, + "exclusions": [], + "filter": "severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-disabled", + "name": "c7n-sink-overlap-disabled", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "exclusion_all": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [ + { + "description": "", + "disabled": false, + "filter": "severity>=ERROR", + "name": "all" + } + ], + "filter": "severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-excl-all", + "name": "c7n-sink-overlap-excl-all", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "exclusion_partial": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [ + { + "description": "", + "disabled": false, + "filter": "resource.type=\"gce_instance\"", + "name": "gce" + } + ], + "filter": "severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-excl-partial", + "name": "c7n-sink-overlap-excl-partial", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "non_match": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "severity=ERROR AND NOT resource.type=\"gce_instance\"", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-not-clause", + "name": "c7n-sink-overlap-not-clause", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + }, + "other_destination": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "pubsub.googleapis.com/projects/cloud-custodian/topics/c7n-sink-overlap-other", + "disabled": false, + "exclusions": [], + "filter": "severity>=ERROR", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-other-dest", + "name": "c7n-sink-overlap-other-dest", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "serviceAccount:service-12345678901@gcp-sa-logging.iam.gserviceaccount.com" + }, + "subset": { + "bigquery_options": [], + "custom_writer_identity": null, + "deletion_policy": "DELETE", + "description": "", + "destination": "logging.googleapis.com/projects/cloud-custodian/locations/global/buckets/_Default", + "disabled": false, + "exclusions": [], + "filter": "severity>=CRITICAL", + "id": "projects/cloud-custodian/sinks/c7n-sink-overlap-subset", + "name": "c7n-sink-overlap-subset", + "project": "cloud-custodian", + "unique_writer_identity": true, + "writer_identity": "" + } + }, + "google_pubsub_topic": { + "other": { + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "id": "projects/cloud-custodian/topics/c7n-sink-overlap-other", + "ingestion_data_source_settings": [], + "kms_key_name": "", + "labels": null, + "message_retention_duration": "", + "message_storage_policy": [], + "message_transforms": [], + "name": "c7n-sink-overlap-other", + "project": "cloud-custodian", + "schema_settings": [], + "tags": null, + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "timeouts": null + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_gcp_utils.py b/tools/c7n_gcp/tests/test_gcp_utils.py new file mode 100644 index 00000000000..bb43fe63bbf --- /dev/null +++ b/tools/c7n_gcp/tests/test_gcp_utils.py @@ -0,0 +1,465 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from c7n_gcp.utils import ( + CloudLoggingOverlap, + canonicalize_cloud_logging_filter, + compare_cloud_logging_filters, + parse_cloud_logging_filter, +) + + +@pytest.mark.parametrize( + "operator,expression", + [ + ("=", 'jsonPayload.user = "alice"'), + ("!=", 'jsonPayload.user != "alice"'), + (">", "httpRequest.status > 199"), + (">=", "httpRequest.status >= 200"), + ("<", "httpRequest.status < 500"), + ("<=", "httpRequest.status <= 499"), + (":", 'textPayload : "timeout"'), + ("=~", 'labels.env =~ "^prod"'), + ("!~", 'protoPayload.methodName !~ "Delete"'), + ], +) +def test_parse_cloud_logging_filter_each_comparison_operator(operator, expression): + parsed = parse_cloud_logging_filter(expression) + assert {p["op"] for p in parsed["predicates"]} == {operator} + assert parsed["comparison_operators"] == [operator] + assert parsed["logical_operators"] == [] + + +@pytest.mark.parametrize( + "logical_op,expression", + [ + ("and", 'severity>=ERROR AND resource.type="gce_instance"'), + ("or", 'severity>=ERROR OR resource.type="gce_instance"'), + ("not", 'NOT resource.type="gce_instance"'), + ], +) +def test_parse_cloud_logging_filter_each_logical_operator(logical_op, expression): + parsed = parse_cloud_logging_filter(expression) + assert parsed["logical_operators"] == [logical_op] + + +def test_parse_cloud_logging_filter_all_operators_together(): + expression = ( + 'NOT (severity>=ERROR AND resource.type="gce_instance") ' + 'OR textPayload:"timeout" ' + 'AND jsonPayload.user!="root" ' + 'AND labels.env=~"^prod" ' + 'AND protoPayload.methodName!~"Delete" ' + 'AND httpRequest.status<500 ' + 'AND httpRequest.status>199 ' + 'AND sample=1 ' + 'AND retries<=2' + ) + parsed = parse_cloud_logging_filter(expression) + + assert parsed["logical_operators"] == ["and", "not", "or"] + assert parsed["comparison_operators"] == sorted( + ["=", "!=", ">", ">=", "<", "<=", ":", "=~", "!~"] + ) + + +def test_canonicalize_cloud_logging_filter_normalizes_structure(): + first = ( + 'NOT (resource.type="gce_instance" OR severity=ERROR)') + + +def test_parse_cloud_logging_filter_not_without_expression_raises(): + with pytest.raises(ValueError, match="Unexpected end of Cloud Logging filter expression"): + parse_cloud_logging_filter("NOT") + + +def test_parse_cloud_logging_filter_missing_closing_paren_raises(): + with pytest.raises(ValueError, match="Missing closing parenthesis"): + parse_cloud_logging_filter('(severity>=ERROR AND resource.type="gce_instance"') + + +def test_parse_cloud_logging_filter_empty_group_raises(): + with pytest.raises(ValueError, match="Empty predicate"): + parse_cloud_logging_filter("()") + + +def test_parse_cloud_logging_filter_malformed_comparison_raises(): + with pytest.raises(ValueError, match="Malformed comparison"): + parse_cloud_logging_filter("severity=") + + +def test_parse_cloud_logging_filter_handles_parenthesized_rhs_expression(): + parsed = parse_cloud_logging_filter('resource.type=("gce_instance")') + assert parsed["predicates"] == [ + {"lhs": "resource.type", "op": "=", "rhs": '"gce_instance"'} + ] + + +def test_parse_cloud_logging_filter_distributes_rhs_value_list(): + parsed = parse_cloud_logging_filter('resource.type=("gce_instance" OR "k8s_container")') + assert parsed["predicates"] == [ + {"lhs": "resource.type", "op": "=", "rhs": '"gce_instance"'}, + {"lhs": "resource.type", "op": "=", "rhs": '"k8s_container"'}, + ] + assert canonicalize_cloud_logging_filter( + 'resource.type=("gce_instance" OR "k8s_container")' + ) == canonicalize_cloud_logging_filter( + 'resource.type="k8s_container" OR resource.type="gce_instance"' + ) + + +def test_parse_cloud_logging_filter_empty_rhs_value_list_raises(): + with pytest.raises(ValueError, match="Malformed comparison"): + parse_cloud_logging_filter("resource.type=()") + + +def test_parse_cloud_logging_filter_lowercase_logical_words_are_search_terms(): + parsed = parse_cloud_logging_filter('severity>=ERROR and resource.type="gce_instance"') + assert parsed["terms"] == ["and"] + assert len(parsed["predicates"]) == 2 + + +def test_canonicalize_cloud_logging_filter_implicit_and_matches_explicit_and(): + assert canonicalize_cloud_logging_filter( + 'severity>=ERROR resource.type="gce_instance"' + ) == canonicalize_cloud_logging_filter( + 'severity>=ERROR AND resource.type="gce_instance"' + ) + assert canonicalize_cloud_logging_filter( + 'a=1 (b=2 OR c=3)' + ) == canonicalize_cloud_logging_filter('a=1 AND (b=2 OR c=3)') + + +def test_canonicalize_cloud_logging_filter_or_binds_tighter_than_and(): + # Example from the Cloud Logging query language docs. + assert canonicalize_cloud_logging_filter( + '"a" OR NOT "b" AND NOT "c" OR "d"' + ) == canonicalize_cloud_logging_filter( + '("a" OR (NOT "b")) AND ((NOT "c") OR "d")' + ) + + +@pytest.mark.parametrize( + "minus,not_", + [ + ('-resource.type="gce_instance"', 'NOT resource.type="gce_instance"'), + ('-(a=1 OR b=2)', 'NOT (a=1 OR b=2)'), + ('severity>=ERROR -resource.type="gce_instance"', + 'severity>=ERROR AND NOT resource.type="gce_instance"'), + ], +) +def test_canonicalize_cloud_logging_filter_minus_is_not(minus, not_): + assert canonicalize_cloud_logging_filter(minus) == canonicalize_cloud_logging_filter(not_) + + +def test_parse_cloud_logging_filter_minus_after_operator_is_a_value(): + parsed = parse_cloud_logging_filter("httpRequest.latency>-1 AND sample=-0.5") + assert [p["rhs"] for p in parsed["predicates"]] == ["-1", "-0.5"] + assert "not" not in parsed["logical_operators"] + + +@pytest.mark.parametrize( + "first,second", + [ + ('a=1 AND (b=2 AND c=3)', '(a=1 AND b=2) AND c=3'), + ('a=1 OR (b=2 OR c=3)', '(a=1 OR b=2) OR c=3'), + ('a=1 AND a=1', 'a=1'), + ], +) +def test_canonicalize_cloud_logging_filter_flattens_nested_groups(first, second): + assert canonicalize_cloud_logging_filter(first) == canonicalize_cloud_logging_filter(second) + + +def test_canonicalize_cloud_logging_filter_is_idempotent(): + expression = ( + 'NOT (resource.type="gce_instance" OR severity=ERROR', 'severity>=error'), + ('"Timeout"', '"timeout"'), + # Built-in field names ignore letter case. + ('SEVERITY>=ERROR', 'severity>=ERROR'), + ('Resource.Type="a"', 'resource.type="a"'), + ('RESOURCE.LABELS.zone="a"', 'resource.labels.zone="a"'), + ('JSONPAYLOAD.userId="a"', 'jsonPayload.userId="a"'), + ], +) +def test_canonicalize_cloud_logging_filter_ignores_value_case(first, second): + assert canonicalize_cloud_logging_filter(first) == canonicalize_cloud_logging_filter(second) + + +@pytest.mark.parametrize( + "first,second", + [ + # Map and struct keys are case-sensitive. + ('labels.Env="prod"', 'labels.env="prod"'), + ('jsonPayload.userId="a"', 'jsonPayload.userid="a"'), + ('resource.labels.Zone="a"', 'resource.labels.zone="a"'), + ('jsonPayload."My Key"="a"', 'jsonPayload."my key"="a"'), + # Regular expressions are case-sensitive. + ('labels.env=~"^Prod"', 'labels.env=~"^prod"'), + ('labels.env!~Prod', 'labels.env!~prod'), + ], +) +def test_canonicalize_cloud_logging_filter_keeps_case_sensitive_parts(first, second): + assert canonicalize_cloud_logging_filter(first) != canonicalize_cloud_logging_filter(second) + + +def test_parse_cloud_logging_filter_handles_parenthesized_lhs_expression(): + parsed = parse_cloud_logging_filter("sample(insertId)=1") + assert parsed["predicates"][0]["lhs"] == "sample(insertId)" + assert parsed["predicates"][0]["op"] == "=" + assert parsed["predicates"][0]["rhs"] == "1" + + +def test_canonicalize_cloud_logging_filter_term_only_expression(): + assert canonicalize_cloud_logging_filter("UNSTRUCTURED_TERM") == "unstructured_term" + + +@pytest.mark.parametrize( + "first,second", + [ + # "--" starts a comment that runs to the end of the line. + ('severity>=ERROR -- was severity=DEBUG', 'severity>=ERROR'), + ('severity>=ERROR\n-- see (ticket\nresource.type="a"', + 'severity>=ERROR AND resource.type="a"'), + # A leading minus applies to a quoted search term as a whole. + ('-"foo bar"', 'NOT "foo bar"'), + # Only a name directly followed by "(" is a function call. + ('unicorn (a=1 OR b=2)', 'unicorn AND (a=1 OR b=2)'), + # A value means the same quoted or not. + ('resource.type=gce_instance', 'resource.type="gce_instance"'), + ("labels.env='prod'", 'labels.env="prod"'), + ], +) +def test_canonicalize_cloud_logging_filter_equivalent_forms(first, second): + assert canonicalize_cloud_logging_filter(first) == canonicalize_cloud_logging_filter(second) + + +@pytest.mark.parametrize( + "first,second", + [ + # Whitespace inside a quoted string is significant. + ('textPayload="a b"', 'textPayload="a b"'), + # "--" inside a quoted string is not a comment. + ('textPayload="a -- b"', 'textPayload="a"'), + ], +) +def test_canonicalize_cloud_logging_filter_distinct_forms(first, second): + assert canonicalize_cloud_logging_filter(first) != canonicalize_cloud_logging_filter(second) + + +@pytest.mark.parametrize( + "expression", + [ + 'LOG_ID("a") OR -LOG_ID("b")', + 'sample(insertId, 0.1)=1', + 'NOT(a=1 OR b=2)', + 'jsonPayload."my key"="x"', + 'textPayload="a b"', + 'httpRequest.latency>-1', + ], +) +def test_canonicalize_cloud_logging_filter_round_trips(expression): + canonical = canonicalize_cloud_logging_filter(expression) + assert canonicalize_cloud_logging_filter(canonical) == canonical + + +def test_parse_cloud_logging_filter_quoted_field_path_segment(): + parsed = parse_cloud_logging_filter('jsonPayload."my key"="x"') + assert parsed["predicates"] == [{"lhs": 'jsonPayload."my key"', "op": "=", "rhs": '"x"'}] + + +def _compare(left, right): + return compare_cloud_logging_filters( + canonicalize_cloud_logging_filter(left), + canonicalize_cloud_logging_filter(right), + ) + + +@pytest.mark.parametrize( + "left,right", + [ + ('severity>=ERROR AND resource.type="gce_instance"', + 'resource.type="gce_instance" AND severity>=ERROR'), + ('', 'severity>=ERROR'), + ('severity>=ERROR', ''), + ('sample=1 AND sample>0', 'sample<=2'), + ('sample>0', 'sample<=10'), + ('severity>=ERROR AND NOT resource.type="gce_instance"', 'severity>=ERROR'), + # OR is evaluated per alternative, including RHS value lists. + ('resource.type=("gce_instance" OR "k8s_container")', 'resource.type="gce_instance"'), + # De Morgan: NOT (a AND b) holds when either is false. + ('NOT (resource.type="a" AND severity>=ERROR)', 'resource.type="a"'), + # Severity names only order the severity field. + ('labels.level="error"', 'labels.level!="500"'), + ('severity!=ERROR', 'severity>=WARNING'), + # Severity is always present, so a negated range is its complement. + ('NOT severity=WARNING'), + # Numbers compare by value. + ('sample=5', 'sample=5.0'), + ('severity=ERROR', 'severity=500'), + # The same string written with different escapes. + ('labels.k="a\\"b"', "labels.k='a\"b'"), + # Comments are ignored. + ('severity>=ERROR -- was severity>=WARNING', 'severity>=ERROR'), + ], +) +def test_compare_cloud_logging_filters_proven_overlap(left, right): + assert _compare(left, right) is CloudLoggingOverlap.OVERLAP + assert _compare(right, left) is CloudLoggingOverlap.OVERLAP + + +@pytest.mark.parametrize( + "left,right", + [ + ('resource.type="gce_instance"', 'resource.type="k8s_container"'), + ('httpRequest.status<200', 'httpRequest.status>=500'), + ('severity>=ERROR', 'severity=10', 'sample<5'), + ('sample<5', 'sample>5'), + ('NOT resource.type="gce_instance"', 'resource.type="gce_instance"'), + ('-resource.type="gce_instance"', 'resource.type="gce_instance"'), + ('severity>=ERROR resource.type="gce_instance"', 'resource.type="k8s_container"'), + ('resource.type=("gce_instance" OR "k8s_container")', + 'resource.type="cloud_run_revision"'), + ('NOT (resource.type="a" OR resource.type="b")', 'resource.type="b"'), + # Shape of the GCP-managed _Required and _Default sinks. + ('LOG_ID("a") OR LOG_ID("b")', 'NOT LOG_ID("a") AND NOT LOG_ID("b")'), + # A negated range is complemented once the field must be present. + ('NOT severity>=ERROR', 'severity>=CRITICAL'), + ('NOT sample!=1 AND sample>0', 'sample=2'), + # Unevaluable literals don't hide a contradiction elsewhere. + ('textPayload:"timeout" AND resource.type="a"', 'resource.type="b"'), + # Severity is one of a fixed set of levels. + ('severity>ERROR', 'severityWARNING', 'severity=WARNING'), + ('sample>5', 'sample=5'), + ('sample<5', 'sample=5'), + ('sample>5', 'sample>=5 AND sample<=5'), + ('sample<5', 'sample>=5 AND sample<=5'), + # Built-in field names ignore letter case. + ('SEVERITY>=ERROR', 'severityprod', 'labels.env=ERROR'), + ('NOT LOG_ID("a") AND NOT LOG_ID("b")', 'severity>=ERROR'), + # A negated range may hold only because the field is absent. + ('NOT httpRequest.status>=500', 'resource.type="gce_instance"'), + # Excluding a value inside a range may empty it for integer fields. + ('httpRequest.status>199 AND httpRequest.status<201', + 'httpRequest.status!=200'), + # Map keys that differ only in case. + ('labels.Env="a"', 'labels.env="b"'), + # A comparison on an array field matches when any element matches. + ('protoPayload.authorizationInfo.permission="storage.objects.get"', + 'protoPayload.authorizationInfo.permission="storage.objects.list"'), + ('jsonPayload.shoeSize<7', 'jsonPayload.shoeSize>9'), + # "true" is an ordinary search term. + ('NOT true', 'resource.type="a"'), + # Values that are not severity levels, or not finite numbers. + ('severity=BOGUS', ''), + ('labels.mode="inf"', 'labels.mode<10'), + ], +) +def test_compare_cloud_logging_filters_possible_overlap(left, right): + assert _compare(left, right) is CloudLoggingOverlap.POSSIBLE + assert _compare(right, left) is CloudLoggingOverlap.POSSIBLE + + +@pytest.mark.parametrize( + "contradiction", + [ + 'severity>=ERROR AND severity<=WARNING', + 'severity>ERROR AND severity=DEFAULT', + # Every entry has one of the fixed severity levels. + ' AND '.join( + f'severity!={level}' for level in ( + 'DEFAULT', 'DEBUG', 'INFO', 'NOTICE', 'WARNING', + 'ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY', + ) + ), + ], +) +def test_compare_cloud_logging_filters_unsatisfiable_filter_overlaps_nothing(contradiction): + assert _compare(contradiction, '') is CloudLoggingOverlap.DISJOINT + assert _compare(contradiction, contradiction) is CloudLoggingOverlap.DISJOINT + + +def test_compare_cloud_logging_filters_oversized_normal_form_is_possible(caplog): + # Nine ANDed two-way ORs expand to 2**9 conjunctions, past the cap. + left = ' AND '.join(f'(oversized{i}="a" OR oversized{i}="b")' for i in range(9)) + right = ' AND '.join(f'oversized{i}="c"' for i in range(9)) + assert _compare(left, right) is CloudLoggingOverlap.POSSIBLE + assert "too complex to compare" in caplog.text diff --git a/tools/c7n_gcp/tests/test_logging.py b/tools/c7n_gcp/tests/test_logging.py index 85e6f93db96..ff4fe828fa6 100644 --- a/tools/c7n_gcp/tests/test_logging.py +++ b/tools/c7n_gcp/tests/test_logging.py @@ -4,6 +4,7 @@ from googleapiclient.errors import HttpError from gcp_common import BaseTest, event_data +from pytest_terraform import terraform class LogProjectSinkTest(BaseTest): @@ -92,6 +93,200 @@ def test_bucket_filter(self): self.assertEqual(len(resources), 1) + def _process_sink_overlap(self, match, sinks): + policy = self.load_policy({ + 'name': 'log-project-sink-overlap-process', + 'resource': 'gcp.log-project-sink', + 'filters': [{'type': 'sink-overlap', 'match': match}]}) + matched = policy.resource_manager.filters[0].process(sinks) + return {s['name']: s['c7n:matched-sinks'] for s in matched} + + def test_sink_overlap_annotates_direct_matches_only(self): + destination = 'logging.googleapis.com/projects/p/locations/global/buckets/b' + matched = self._process_sink_overlap('overlap', [ + {'name': 'low', 'destination': destination, 'filter': 'severity<=ERROR'}, + {'name': 'mid', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'high', 'destination': destination, 'filter': 'severity>=CRITICAL'}, + ]) + # low and high only overlap through mid, so they are not peers. + self.assertEqual(matched, { + 'low': ['mid'], + 'mid': ['low', 'high'], + 'high': ['mid'], + }) + + def test_sink_overlap_annotates_possible_matches_separately(self): + destination = 'logging.googleapis.com/projects/p/locations/global/buckets/b' + policy = self.load_policy({ + 'name': 'log-project-sink-overlap-possible', + 'resource': 'gcp.log-project-sink', + 'filters': [{'type': 'sink-overlap'}]}) + matched = policy.resource_manager.filters[0].process([ + {'name': 'errors', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'critical', 'destination': destination, 'filter': 'severity>=CRITICAL'}, + {'name': 'timeouts', 'destination': destination, 'filter': 'textPayload:"timeout"'}, + {'name': 'timeouts-copy', 'destination': destination, + 'filter': 'textPayload:"timeout"'}, + {'name': 'debug', 'destination': destination, 'filter': 'severity<=DEBUG'}, + ]) + annotations = { + s['name']: (s['c7n:matched-sinks'], s['c7n:possible-matched-sinks']) + for s in matched + } + self.assertEqual(annotations, { + 'errors': (['critical'], ['timeouts', 'timeouts-copy']), + 'critical': (['errors'], ['timeouts', 'timeouts-copy']), + # Identical filters are proven duplicates even when unevaluable. + 'timeouts': (['timeouts-copy'], ['errors', 'critical', 'debug']), + 'timeouts-copy': (['timeouts'], ['errors', 'critical', 'debug']), + 'debug': ([], ['timeouts', 'timeouts-copy']), + }) + + def test_sink_overlap_unparseable_filters_do_not_fail_the_policy(self): + destination = 'logging.googleapis.com/projects/p/locations/global/buckets/b' + + def sinks(): + return [ + {'name': 'base', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'copy', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'bad', 'destination': destination, + 'filter': 'severity>=ERROR AND (resource.type="gce_instance"'}, + {'name': 'bad-copy', 'destination': destination, + 'filter': 'severity>=ERROR AND (resource.type="gce_instance"'}, + # Whitespace inside a quoted string is significant. + {'name': 'bad-spaced', 'destination': destination, + 'filter': 'severity>=ERROR AND (resource.type="gce instance"'}, + {'name': 'bad-spaced-copy', 'destination': destination, + 'filter': 'severity>=ERROR AND (resource.type="gce instance"'}, + {'name': 'bad-exclusion', 'destination': destination, + 'filter': 'severity>=ERROR', + 'exclusions': [{'name': 'broken', 'filter': 'resource.type="gce_instance")'}]}, + ] + + with self.assertLogs('custodian.filters', level='WARNING') as logs: + overlap = self._process_sink_overlap('overlap', sinks()) + self.assertEqual(len(logs.records), 5) + self.assertIn('log sink bad:', logs.output[0]) + # Unparseable sinks are left out of overlap comparisons. + self.assertEqual(overlap, {'base': ['copy'], 'copy': ['base']}) + + with self.assertLogs('custodian.filters', level='WARNING'): + exact = self._process_sink_overlap('exact', sinks()) + # Identical unparseable filters, ignoring whitespace outside quoted + # strings, are duplicates. + self.assertEqual(exact, { + 'base': ['copy'], + 'copy': ['base'], + 'bad': ['bad-copy'], + 'bad-copy': ['bad'], + }) + + def test_sink_overlap_ignores_comments(self): + destination = 'logging.googleapis.com/projects/p/locations/global/buckets/b' + + def sinks(): + return [ + {'name': 'base', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'commented', 'destination': destination, + 'filter': 'severity>=ERROR -- was severity>=WARNING (see ticket'}, + ] + + # A commented filter used to produce a key that failed to re-parse. + for match in ('overlap', 'exact'): + self.assertEqual(self._process_sink_overlap(match, sinks()), { + 'base': ['commented'], + 'commented': ['base'], + }) + + def test_sink_overlap_applies_enabled_exclusions(self): + destination = 'logging.googleapis.com/projects/p/locations/global/buckets/b' + + def sinks(): + return [ + {'name': 'base', 'destination': destination, 'filter': 'severity>=ERROR'}, + {'name': 'excluded', 'destination': destination, 'filter': 'severity>=ERROR', + 'exclusions': [{'name': 'all', 'filter': 'severity>=ERROR'}]}, + {'name': 'disabled-exclusion', 'destination': destination, + 'filter': 'severity>=ERROR', + 'exclusions': [{'name': 'all', 'filter': 'severity>=ERROR', + 'disabled': True}]}, + {'name': 'partial-exclusion', 'destination': destination, + 'filter': 'severity>=ERROR', + 'exclusions': [{'name': 'gce', 'filter': 'resource.type="gce_instance"'}]}, + ] + + # An exclusion that removes every entry leaves nothing to overlap. + self.assertEqual(self._process_sink_overlap('overlap', sinks()), { + 'base': ['disabled-exclusion', 'partial-exclusion'], + 'disabled-exclusion': ['base', 'partial-exclusion'], + 'partial-exclusion': ['base', 'disabled-exclusion'], + }) + # Only sinks with the same filter and enabled exclusions are duplicates. + self.assertEqual(self._process_sink_overlap('exact', sinks()), { + 'base': ['disabled-exclusion'], + 'disabled-exclusion': ['base'], + }) + + +@terraform('log_project_sink_exact') +def test_log_project_sink_exact(test, log_project_sink_exact): + def name(resource): + return log_project_sink_exact[f'google_logging_project_sink.{resource}.name'] + + factory = test.replay_flight_data('log-project-sink-exact') + policy = test.load_policy( + { + 'name': 'log-project-sink-exact', + 'resource': 'gcp.log-project-sink', + 'filters': [ + {'type': 'value', 'key': 'name', 'op': 'regex', 'value': '^c7n-sink-exact-'}, + {'type': 'sink-overlap', 'match': 'exact'}, + ], + }, + session_factory=factory, + ) + resources = {r['name']: r for r in policy.run()} + + # Not matched: a different filter (non_match), disabled, an extra + # exclusion (with_exclusion) and a different destination (other_destination). + assert set(resources) == {name('a'), name('b'), name('no_filter_a'), name('no_filter_b')} + assert resources[name('a')]['c7n:matched-sinks'] == [name('b')] + assert resources[name('no_filter_a')]['c7n:matched-sinks'] == [name('no_filter_b')] + + +@terraform('log_project_sink_overlap') +def test_log_project_sink_overlap(test, log_project_sink_overlap): + def name(resource): + return log_project_sink_overlap[f'google_logging_project_sink.{resource}.name'] + + factory = test.replay_flight_data('log-project-sink-overlap') + policy = test.load_policy( + { + 'name': 'log-project-sink-overlap', + 'resource': 'gcp.log-project-sink', + 'filters': [ + {'type': 'value', 'key': 'name', 'op': 'regex', 'value': '^c7n-sink-overlap-'}, + {'type': 'sink-overlap'}, + ], + }, + session_factory=factory, + ) + resources = {r['name']: r for r in policy.run()} + + # Not matched: severity Date: Wed, 14 Oct 2026 13:03:22 -0500 Subject: [PATCH 43/65] azure - vm - allow resize to accept a map (#11191) --------- Co-authored-by: Claude Opus 5.5 --- tools/c7n_azure/c7n_azure/resources/vm.py | 77 +- tools/c7n_azure/tests_azure/azure_common.py | 70 +- .../cassettes/VMResizeTest.test_resize.json | 1305 +++++++++++ .../VMResizeTest.test_resize_type_map.json | 1984 +++++++++++++++++ ...sizeTest.test_resize_type_map_default.json | 1305 +++++++++++ .../tests_azure/templates/vm-resize.json | 112 + .../tests_azure/test_azure_common.py | 1 + .../tests_azure/tests_resources/test_vm.py | 250 ++- 8 files changed, 5060 insertions(+), 44 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map.json create mode 100644 tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map_default.json create mode 100644 tools/c7n_azure/tests_azure/templates/vm-resize.json diff --git a/tools/c7n_azure/c7n_azure/resources/vm.py b/tools/c7n_azure/c7n_azure/resources/vm.py index d0c9348bb89..a195e6d0a4d 100644 --- a/tools/c7n_azure/c7n_azure/resources/vm.py +++ b/tools/c7n_azure/c7n_azure/resources/vm.py @@ -5,6 +5,7 @@ from c7n_azure.provider import resources from c7n_azure.resources.arm import ArmResourceManager +from c7n.exceptions import PolicyValidationError from c7n.filters.core import ValueFilter, type_schema, ListItemFilter from c7n.filters.related import RelatedResourceFilter from c7n.utils import local_session @@ -414,27 +415,91 @@ class VmResizeAction(AzureBaseAction): actions: - type: resize vmSize: Standard_A2_v2 + + :example: + + Resize VMs based on their current size. ``type-map`` keys are matched + case-insensitively against the VM's current size. VMs whose size is not + in ``type-map`` are resized to ``default`` if given, otherwise skipped. + + .. code-block:: yaml + + policies: + - name: resize-vms-by-size + resource: azure.vm + filters: + - "tag:rightsize": "true" + actions: + - type: resize + type-map: + Standard_D4s_v3: Standard_D2s_v3 + Standard_D8s_v3: Standard_D4s_v3 + + .. warning:: + + ``default`` resizes **every** VM matched by the policy's filters whose + current size is not a key in ``type-map``, regardless of what that + size is. This can downsize production workloads, upsize VMs into a + more expensive size, or move VMs to a size that lacks capacity they + depend on (vCPUs, memory, data disk count, accelerated networking, + premium storage, etc.). Using ``default`` without ``type-map`` resizes + every matched VM to one size. Resizing restarts a running VM. + + Only use ``default`` with filters that narrow the policy to VMs you + intend to resize, and prefer an explicit ``type-map`` entry for each + size you expect. Running the policy with ``--dryrun`` first shows + which VMs would be affected. """ schema = type_schema( 'resize', - required=['vmSize'], **{ - 'vmSize': {'type': 'string'} + 'vmSize': {'type': 'string', 'minLength': 1}, + 'type-map': { + 'type': 'object', + 'minProperties': 1, + 'additionalProperties': {'type': 'string', 'minLength': 1} + }, + 'default': {'type': 'string', 'minLength': 1} }) - def __init__(self, data, manager=None): - super(VmResizeAction, self).__init__(data, manager) - self.vm_size = self.data['vmSize'] + def validate(self): + has_mapping = 'type-map' in self.data or 'default' in self.data + if 'vmSize' in self.data and has_mapping: + raise PolicyValidationError( + "%s: vmSize cannot be combined with type-map or default on %s" % ( + self.manager.data['name'], self.type)) + if 'vmSize' not in self.data and not has_mapping: + raise PolicyValidationError( + "%s: one of vmSize, type-map, or default is required on %s" % ( + self.manager.data['name'], self.type)) + return self def _prepare_processing(self): self.client = self.manager.get_client() + self.type_map = { + k.lower(): v for k, v in self.data.get('type-map', {}).items()} + + def _get_target_size(self, current_size): + if 'vmSize' in self.data: + return self.data['vmSize'] + return self.type_map.get( + (current_size or '').lower(), self.data.get('default')) def _process_resource(self, resource): - hardware_profile = HardwareProfile(vm_size=self.vm_size) + current_size = resource.get( + 'properties', {}).get('hardwareProfile', {}).get('vmSize') + target_size = self._get_target_size(current_size) + if not target_size: + return f'not resized, no target size for {current_size}' + if current_size and target_size.lower() == current_size.lower(): + return f'not resized, already {current_size}' + + hardware_profile = HardwareProfile(vm_size=target_size) self.client.virtual_machines.begin_update( resource['resourceGroup'], resource['name'], VirtualMachineUpdate(hardware_profile=hardware_profile) ) + return f'resized from {current_size} to {target_size}' diff --git a/tools/c7n_azure/tests_azure/azure_common.py b/tools/c7n_azure/tests_azure/azure_common.py index a65c50c825c..f59fecacfaa 100644 --- a/tools/c7n_azure/tests_azure/azure_common.py +++ b/tools/c7n_azure/tests_azure/azure_common.py @@ -1,5 +1,6 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from collections import Counter import datetime import email.utils as eut from functools import wraps @@ -9,7 +10,7 @@ import re from time import sleep from unittest.mock import patch -from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit, urlparse import azure.core.polling from msrest.pipeline import ClientRawResponse @@ -172,13 +173,63 @@ def setUp(self): self.addCleanup(self._assert_cassette_fully_played) def _assert_cassette_fully_played(self): - unplayed = [ - '{0} {1}'.format(r.method, r.uri) - for i, r in enumerate(self.cassette.requests) - if not self.cassette.play_counts[i] - ] + poll_paths, final_gets = self._lro_poller_requests() + unplayed = [] + for i, r in enumerate(self.cassette.requests): + if self.cassette.play_counts[i]: + continue + path = urlparse(r.uri).path.lower() + if r.method == 'GET' and path in poll_paths: + continue + # Playback matches a test's own GET of the resource to whichever + # recorded GET of that path comes first, which may be the poller's, + # so exempt any unplayed GET of the path up to the poller's count. + if r.method == 'GET' and final_gets[path]: + final_gets[path] -= 1 + continue + unplayed.append('{0} {1}'.format(r.method, r.uri)) self.assertFalse(unplayed, 'unplayed cassette interactions') + def _lro_poller_requests(self): + """Requests the LRO poller made while the cassette was recorded. + + A recording captures whatever the poller did before the test finished, + but playback disables polling unless the test requires it, so these + can never be replayed. Returns the status URL paths the operations' + initial responses pointed to, and a count per resource path of the + final GETs the poller recorded, each being the next GET of a PUT or + PATCH operation's resource once its status was polled. The request + that started each operation must still be played. + """ + poll_paths, final_gets = set(), Counter() + if getattr(self, '_requires_polling', False): + return poll_paths, final_gets + # Resource path of each PUT or PATCH operation -> its status paths + operations = {} + polled = set() + for request, response in self.cassette.data: + path = urlparse(request.uri).path.lower() + if request.method == 'GET': + if path in poll_paths: + polled.update( + resource for resource, status_paths in operations.items() + if path in status_paths + ) + elif path in polled: + final_gets[path] += 1 + polled.discard(path) + del operations[path] + continue + headers = response.get('headers') or {} + urls = list(headers.get('azure-asyncoperation', [])) + if response.get('status', {}).get('code') == 202: + urls.extend(headers.get('location', [])) + status_paths = {urlparse(url).path.lower() for url in urls} + poll_paths.update(status_paths) + if status_paths and request.method in ('PUT', 'PATCH'): + operations[path] = status_paths + return poll_paths, final_gets + def is_playback(self): # You can't do this in setup because it is actually required by the base class # setup (via our callbacks), but it is also not possible to do until the base class setup @@ -408,7 +459,7 @@ def _replace_async_operation_signature(value): if not any( segment in path for segment in ('operationsstatus/', 'operationstatuses/', - 'operationresults/') + 'operationresults/', '/operations/') ): return value @@ -681,6 +732,11 @@ def strict_cassette(name): the action does nothing at all, because its other assertions read recorded responses either way. + Requests the long-running operation poller recorded (status polls, and + the final GET after a PUT or PATCH) are exempt when playback disables + polling (the default without ``requires_arm_polling``), since they cannot + be replayed; the request that started the operation is not. + Mutually exclusive with sharing a cassette between tests, since a shared recording is the union of what each of them replays. """ diff --git a/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize.json b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize.json new file mode 100644 index 00000000000..b13852e5c1b --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize.json @@ -0,0 +1,1305 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "8528" + ], + "x-msedge-ref": [ + "Ref A: CAC607387019439F96F9E9374F8FAD24 Ref B: CHI261140505031 Ref C: 2026-10-14T15:31:59Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-original-request-ids": [ + "a10d1016-a3a6-4904-8d64-66b07c39edc0" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:31:59 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;899" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"4\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"6\"" + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2191" + ], + "x-msedge-ref": [ + "Ref A: 3A45F5CF176747F6AB921D58257B5EEF Ref B: CHI261140501025 Ref C: 2026-10-14T15:32:00Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"6\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:31:59 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23998,Microsoft.Compute/LowCostGetResource;34" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"6\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "8528" + ], + "x-msedge-ref": [ + "Ref A: 2765F5F8175B497A85E17A3866E34C07 Ref B: CHI261140512042 Ref C: 2026-10-14T15:32:00Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-original-request-ids": [ + "02729270-2124-4b97-863c-7fc94f71c6e3" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:00 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;898" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"4\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"6\"" + } + ] + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize?api-version=2024-11-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "content-length": [ + "2191" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-msedge-ref": [ + "Ref A: C6B1AE9F23324D8C8E914E25C74646B7 Ref B: CHI261140502036 Ref C: 2026-10-14T15:32:01Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:00 GMT" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/ed80d23b-ad9b-4ae3-8dbc-d60a4f6fcba3?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887216587266&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/UpdateVMSubscriptionMaximum;1499,Microsoft.Compute/UpdateVMResource;11" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Updating", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"7\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/ed80d23b-ad9b-4ae3-8dbc-d60a4f6fcba3?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887216587266&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "134" + ], + "x-msedge-ref": [ + "Ref A: A3FFC833E4DA4517897E1B1795C8891C Ref B: CHI261140501034 Ref C: 2026-10-14T15:32:01Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:02 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;44,Microsoft.Compute/GetOperationSubscriptionMaximum;14995" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T15:32:01.4567267+00:00", + "status": "InProgress", + "name": "ed80d23b-ad9b-4ae3-8dbc-d60a4f6fcba3" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/ed80d23b-ad9b-4ae3-8dbc-d60a4f6fcba3?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887216587266&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "184" + ], + "x-msedge-ref": [ + "Ref A: 02E69E41F2584043BC3DF644349EF3C6 Ref B: CHI261140507052 Ref C: 2026-10-14T15:32:32Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:34 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;43,Microsoft.Compute/GetOperationSubscriptionMaximum;14999" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T15:32:01.4567267+00:00", + "endTime": "2026-10-14T15:32:26.8165499+00:00", + "status": "Succeeded", + "name": "ed80d23b-ad9b-4ae3-8dbc-d60a4f6fcba3" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2192" + ], + "x-msedge-ref": [ + "Ref A: B5D65FEB5C614B4E808754FB5C0DB606 Ref B: CHI261140512062 Ref C: 2026-10-14T15:32:35Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:35 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23999,Microsoft.Compute/LowCostGetResource;35" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"7\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2192" + ], + "x-msedge-ref": [ + "Ref A: 81409282004E41E08419BC0CD3220F7F Ref B: CHI261140505025 Ref C: 2026-10-14T15:32:35Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:32:35 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23998,Microsoft.Compute/LowCostGetResource;34" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"7\"" + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map.json b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map.json new file mode 100644 index 00000000000..fc4b364a390 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map.json @@ -0,0 +1,1984 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "dee0d68e-b19c-44fa-ac31-213a185950b1" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:03 GMT" + ], + "content-length": [ + "8528" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;899" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: A08C22FDBE694CB1AE6D41786401EB90 Ref B: CH1AA2020620011 Ref C: 2026-10-14T17:00:02Z" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"5\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"8\"" + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:03 GMT" + ], + "content-length": [ + "2191" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23999,Microsoft.Compute/LowCostGetResource;35" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 131C3248A1F24B97A7E42A890CDB5428 Ref B: CHI261140514029 Ref C: 2026-10-14T17:00:03Z" + ], + "etag": [ + "\"8\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"8\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:03 GMT" + ], + "content-length": [ + "2198" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23998,Microsoft.Compute/LowCostGetResource;35" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: A5BFCB78A4A841AB8144DEA9948924EB Ref B: CHI261140506031 Ref C: 2026-10-14T17:00:04Z" + ], + "etag": [ + "\"8\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"8\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:03 GMT" + ], + "content-length": [ + "2180" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23997,Microsoft.Compute/LowCostGetResource;35" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: C4C6E4D9A11546E4832BAEA9C1CDF8A0 Ref B: CHI261140506031 Ref C: 2026-10-14T17:00:04Z" + ], + "etag": [ + "\"5\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"5\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "x-ms-original-request-ids": [ + "cd5a7f51-1137-49f4-b7a0-670b3266a48c" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:04 GMT" + ], + "content-length": [ + "8528" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;898" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 993D4CCD908D4B10B9ABF8A880827DDE Ref B: CHI261140504062 Ref C: 2026-10-14T17:00:04Z" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"8\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"5\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"8\"" + } + ] + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms?api-version=2024-11-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/28c975ef-fbaa-43db-a292-c6bffa0440c0?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275940056794564&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/UpdateVMSubscriptionMaximum;1499,Microsoft.Compute/UpdateVMResource;11" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 5E73CBFD177F44F0A6EB9F5930496B7F Ref B: CHI261140507023 Ref C: 2026-10-14T17:00:05Z" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:05 GMT" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "content-length": [ + "2196" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Updating", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s?api-version=2024-11-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/74bd5f0d-3a03-4bf2-9b98-0a8e2e2a60fd?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275940063330463&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/UpdateVMSubscriptionMaximum;1498,Microsoft.Compute/UpdateVMResource;11" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: B2606E4F8C934B17925A17CDFEA65AB1 Ref B: CHI261140509060 Ref C: 2026-10-14T17:00:05Z" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:06 GMT" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "content-length": [ + "2191" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Updating", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/74bd5f0d-3a03-4bf2-9b98-0a8e2e2a60fd?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275940063330463&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:06 GMT" + ], + "content-length": [ + "134" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;44,Microsoft.Compute/GetOperationSubscriptionMaximum;14994" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 0B1EC713CB494E8993CD8697A29ED100 Ref B: CH1AA2020610053 Ref C: 2026-10-14T17:00:06Z" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T17:00:06.1728535+00:00", + "status": "InProgress", + "name": "74bd5f0d-3a03-4bf2-9b98-0a8e2e2a60fd" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/28c975ef-fbaa-43db-a292-c6bffa0440c0?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275940056794564&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:36 GMT" + ], + "content-length": [ + "184" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;43,Microsoft.Compute/GetOperationSubscriptionMaximum;14999" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: DD0EC33F69714CBCBAE4064E0D4BD602 Ref B: CHI261140512062 Ref C: 2026-10-14T17:00:36Z" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T17:00:05.4863038+00:00", + "endTime": "2026-10-14T17:00:30.5034334+00:00", + "status": "Succeeded", + "name": "28c975ef-fbaa-43db-a292-c6bffa0440c0" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:36 GMT" + ], + "content-length": [ + "2197" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23999,Microsoft.Compute/LowCostGetResource;31" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 72D182A6A69C42ACA9547D618A5CCD1A Ref B: CHI261140511029 Ref C: 2026-10-14T17:00:36Z" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:36 GMT" + ], + "content-length": [ + "2192" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23998,Microsoft.Compute/LowCostGetResource;35" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 53250C2A0E934F02AF5B7205F5AE9BB3 Ref B: CHI261140502036 Ref C: 2026-10-14T17:00:37Z" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:37 GMT" + ], + "content-length": [ + "2192" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23997,Microsoft.Compute/LowCostGetResource;34" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: B21913B3F0654586895BC5E190E16FDD Ref B: CHI261140507029 Ref C: 2026-10-14T17:00:37Z" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:37 GMT" + ], + "content-length": [ + "2197" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23996,Microsoft.Compute/LowCostGetResource;30" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 64D0753748B34514903241B84803AA90 Ref B: CH1AA2030813054 Ref C: 2026-10-14T17:00:37Z" + ], + "etag": [ + "\"9\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"9\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "date": [ + "Wed, 14 Oct 2026 17:00:37 GMT" + ], + "content-length": [ + "2180" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23995,Microsoft.Compute/LowCostGetResource;34" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 892E08EB48D047AE8231DD40A80EA3FB Ref B: CHI261140511036 Ref C: 2026-10-14T17:00:38Z" + ], + "etag": [ + "\"5\"" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"5\"" + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map_default.json b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map_default.json new file mode 100644 index 00000000000..c89ac20d53e --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/VMResizeTest.test_resize_type_map_default.json @@ -0,0 +1,1305 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "8529" + ], + "x-msedge-ref": [ + "Ref A: D7791C2C55EA473F82140D1B6DDCEB48 Ref B: CHI261140502052 Ref C: 2026-10-14T15:33:14Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-original-request-ids": [ + "0d44b2d2-7903-4a7f-b847-47a52e1b17f2" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:14 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;898" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"7\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"7\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"4\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"7\"" + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2191" + ], + "x-msedge-ref": [ + "Ref A: 7735212ECC8346389C8B3BFB51A84DA7 Ref B: CHI261140508036 Ref C: 2026-10-14T15:33:14Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"6\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:14 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23983,Microsoft.Compute/LowCostGetResource;35" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"6\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "8529" + ], + "x-msedge-ref": [ + "Ref A: B23E163CD23C4A70AAEBB9736AB38E07 Ref B: CHI261140501025 Ref C: 2026-10-14T15:33:15Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-original-request-ids": [ + "dc2896cc-bc22-418b-831b-76749e2bbedf" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:15 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/HighCostGetSubscriptionMaximum;897" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"6\"" + }, + { + "name": "cctestvmresize-map-b1ms", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1ms", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1s" + }, + "provisioningState": "Succeeded", + "vmId": "95f0706a-91ba-4eef-a179-29d6724d7f5f", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1ms_disk1_be27e6f9e3e543939b8443380fb7c2b5" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1ms", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1ms-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9916035+00:00" + }, + "etag": "\"7\"" + }, + { + "name": "cctestvmresize-map-b1s", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-b1s", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "cb6e2a97-53e4-40ff-9e72-c46377f19315", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-b1s_disk1_e1fe4673e38a48b289fa3c4e9d3cb12f" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-b1s", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-b1s-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0123878+00:00" + }, + "etag": "\"7\"" + }, + { + "name": "cctestvmresize-map-unmapped", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-map-unmapped", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + }, + "provisioningState": "Succeeded", + "vmId": "ab3e10a7-6b18-4522-8c47-76283b6cb932", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-map-unmapped_OsDisk_1_3e2d2133c2cd433687349fc1f02508ea" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-map-unmapped", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + } + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-map-unmapped-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0649567+00:00" + }, + "etag": "\"4\"" + }, + { + "name": "cctestvmresize-vmsize", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-vmsize", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "99c956d6-1c4f-452e-99b2-13f669e3b242", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-vmsize_OsDisk_1_f70bd18f4b314d8fb2eeaedfb5737318" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-vmsize", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-vmsize-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:12.0020458+00:00" + }, + "etag": "\"7\"" + } + ] + } + } + } + }, + { + "request": { + "method": "PATCH", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default?api-version=2024-11-01", + "body": "mock_body", + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "x-ms-ratelimit-remaining-subscription-writes": [ + "199" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "content-length": [ + "2191" + ], + "x-ms-ratelimit-remaining-subscription-global-writes": [ + "2999" + ], + "x-msedge-ref": [ + "Ref A: 42E41484A403470CA011869C379E982E Ref B: CHI261140501034 Ref C: 2026-10-14T15:33:15Z" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:17 GMT" + ], + "azure-asyncnotification": [ + "Enabled" + ], + "azure-asyncoperation": [ + "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/b76ce4e6-f7e9-4b8f-8f6d-139dd0cf3485?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887976662761&c=redacted&s=redacted&h=redacted" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/UpdateVMSubscriptionMaximum;1499,Microsoft.Compute/UpdateVMResource;11" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Updating", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"7\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/b76ce4e6-f7e9-4b8f-8f6d-139dd0cf3485?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887976662761&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "134" + ], + "x-msedge-ref": [ + "Ref A: 0B103F61A5A245DC9D747D9286AEC25A Ref B: CHI261140502054 Ref C: 2026-10-14T15:33:17Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:17 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;44,Microsoft.Compute/GetOperationSubscriptionMaximum;14999" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T15:33:16.0770523+00:00", + "status": "InProgress", + "name": "b76ce4e6-f7e9-4b8f-8f6d-139dd0cf3485" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/locations/southcentralus/operations/b76ce4e6-f7e9-4b8f-8f6d-139dd0cf3485?p=61484f5e-bd98-4b59-8e50-ecdab74043fd&api-version=2024-11-01&t=639275887976662761&c=redacted&s=redacted&h=redacted", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "184" + ], + "x-msedge-ref": [ + "Ref A: 41DB429CB99A42E2AFFC21A1ED773D36 Ref B: CHI261140502023 Ref C: 2026-10-14T15:33:48Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:48 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/GetOperationResource;43,Microsoft.Compute/GetOperationSubscriptionMaximum;14998" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "startTime": "2026-10-14T15:33:16.0770523+00:00", + "endTime": "2026-10-14T15:33:41.0195519+00:00", + "status": "Succeeded", + "name": "b76ce4e6-f7e9-4b8f-8f6d-139dd0cf3485" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2192" + ], + "x-msedge-ref": [ + "Ref A: 471D24CF501649E29D0933BE8EE87E3F Ref B: CHI261140508034 Ref C: 2026-10-14T15:33:48Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:48 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23997,Microsoft.Compute/LowCostGetResource;33" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM-RESIZE/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"7\"" + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default?api-version=2024-11-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "" + }, + "headers": { + "content-length": [ + "2192" + ], + "x-msedge-ref": [ + "Ref A: AAE5722F284E42D98EEE1E84ADED6900 Ref B: CHI261140503034 Ref C: 2026-10-14T15:33:48Z" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-need-to-refresh-epl-cache": [ + "False" + ], + "etag": [ + "\"7\"" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "date": [ + "Wed, 14 Oct 2026 15:33:48 GMT" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-ratelimit-remaining-resource": [ + "Microsoft.Compute/LowCostGetSubscriptionMaximum;23996,Microsoft.Compute/LowCostGetResource;32" + ], + "cache-control": [ + "no-cache" + ] + }, + "body": { + "data": { + "name": "cctestvmresize-default", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/virtualMachines/cctestvmresize-default", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue" + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B1ms" + }, + "provisioningState": "Succeeded", + "vmId": "7572b5ea-f4d0-49a6-a363-cf6784fb2fbe", + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest", + "exactVersion": "22.04.202608060" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Compute/disks/cctestvmresize-default_disk1_b2fe9e6139ad4749aa470ffeee8b1788" + }, + "deleteOption": "Delete", + "diskSizeGB": 30 + }, + "dataDisks": [], + "diskControllerType": "SCSI" + }, + "osProfile": { + "computerName": "cctestvmresize-default", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "provisionVMAgent": true, + "patchSettings": { + "patchMode": "ImageDefault", + "assessmentMode": "ImageDefault" + }, + "enableVMAgentPlatformUpdates": true + }, + "secrets": [], + "allowExtensionOperations": true, + "requireGuestProvisionSignal": true, + "adminUsername": "testuser" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm-resize/providers/Microsoft.Network/networkInterfaces/cctestvmresize-default-nic" + } + ] + }, + "timeCreated": "2026-10-14T01:05:11.9802308+00:00" + }, + "etag": "\"7\"" + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/vm-resize.json b/tools/c7n_azure/tests_azure/templates/vm-resize.json new file mode 100644 index 00000000000..d89265c9185 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/vm-resize.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + + "variables": { + "vms": [ + {"name": "cctestvmresize-vmsize", "size": "Standard_B1s"}, + {"name": "cctestvmresize-default", "size": "Standard_B1s"}, + {"name": "cctestvmresize-map-b1s", "size": "Standard_B1s"}, + {"name": "cctestvmresize-map-b1ms", "size": "Standard_B1ms"}, + {"name": "cctestvmresize-map-unmapped", "size": "Standard_B2s"} + ], + "virtualNetworkName": "cctestvmresize-vnet", + "subnetName": "default", + "subnetRef": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('subnetName'))]" + }, + "resources": [ + { + "apiVersion": "2023-04-01", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[resourceGroup().location]", + "properties": { + "addressSpace": { + "addressPrefixes": ["10.0.0.0/16"] + }, + "subnets": [ + { + "name": "[variables('subnetName')]", + "properties": { + "addressPrefix": "10.0.0.0/24" + } + } + ] + } + }, + { + "apiVersion": "2023-04-01", + "type": "Microsoft.Network/networkInterfaces", + "name": "[concat(variables('vms')[copyIndex()].name, '-nic')]", + "location": "[resourceGroup().location]", + "copy": { + "name": "nicLoop", + "count": "[length(variables('vms'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + } + }, + { + "apiVersion": "2023-03-01", + "type": "Microsoft.Compute/virtualMachines", + "name": "[variables('vms')[copyIndex()].name]", + "location": "[resourceGroup().location]", + "copy": { + "name": "vmLoop", + "count": "[length(variables('vms'))]" + }, + "tags": { + "testtag": "testvalue" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vms')[copyIndex()].name, '-nic'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vms')[copyIndex()].size]" + }, + "osProfile": { + "computerName": "[variables('vms')[copyIndex()].name]", + "adminUsername": "testuser", + "adminPassword": "[concat(uniquestring(resourceGroup().id), '.!Aa1')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "0001-com-ubuntu-server-jammy", + "sku": "22_04-lts-gen2", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage", + "deleteOption": "Delete", + "managedDisk": { + "storageAccountType": "Standard_LRS" + } + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('vms')[copyIndex()].name, '-nic'))]" + } + ] + } + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/test_azure_common.py b/tools/c7n_azure/tests_azure/test_azure_common.py index d7ac24b1e87..d232006b4d1 100644 --- a/tools/c7n_azure/tests_azure/test_azure_common.py +++ b/tools/c7n_azure/tests_azure/test_azure_common.py @@ -158,6 +158,7 @@ def test_recording_sanitizer_redacts_every_operation_url_shape(self): 'operationsStatus', 'operationStatuses', 'operationResults', + 'locations/southcentralus/operations', ): with self.subTest(path=path): url = ( diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_vm.py b/tools/c7n_azure/tests_azure/tests_resources/test_vm.py index 01f0a3c3f37..486f91d0945 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_vm.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_vm.py @@ -2,12 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 import datetime -from azure.mgmt.compute.models import HardwareProfile, VirtualMachineUpdate -from ..azure_common import BaseTest, arm_template, cassette_name +import pytest +from ..azure_common import BaseTest, arm_template, cassette_name, strict_cassette from c7n_azure.session import Session from dateutil import tz as tzutils +from jsonschema.exceptions import ValidationError from unittest.mock import patch +from c7n.exceptions import PolicyValidationError from c7n.testing import mock_datetime_now from c7n.utils import local_session @@ -218,35 +220,6 @@ def test_restart(self, filter_mock): self.fake_running_vms[0]['resourceGroup'], self.fake_running_vms[0]['name']) - @arm_template('vm.json') - @cassette_name('virtual_machine') - @patch('c7n_azure.resources.vm.InstanceViewFilter.process', return_value=fake_running_vms) - def test_resize(self, resize_action_mock): - with patch(self._get_vm_client_string() + '.begin_update') as resize_action_mock: - p = self.load_policy({ - 'name': 'test-azure-vm', - 'resource': 'azure.vm', - 'filters': [ - {'type': 'value', - 'key': 'name', - 'op': 'eq', - 'value_type': 'normalize', - 'value': 'cctestvm'}], - 'actions': [ - {'type': 'resize', - 'vmSize': 'Standard_A2_v2'} - ] - }) - p.run() - - expected_hardware_profile = HardwareProfile(vm_size='Standard_A2_v2') - - resize_action_mock.assert_called_with( - self.fake_running_vms[0]['resourceGroup'], - self.fake_running_vms[0]['name'], - VirtualMachineUpdate(hardware_profile=expected_hardware_profile) - ) - @arm_template('vm.json') @cassette_name('virtual_machine') @patch('c7n_azure.resources.vm.InstanceViewFilter.process', return_value=fake_running_vms) @@ -392,3 +365,218 @@ def test_vm_jit_policy_port_filter(self): self.assertEqual(len(resources), 1) self.assertEqual(resources[0]['name'], 'cctestvm') self.assertEqual(resources[0]['c7n:JitPolicyPorts'][0]['number'], 22) + + +class VMResizeTest(BaseTest): + """Live tests for the azure.vm resize action. + + The vm-resize.json fixture provisions dedicated VMs for each test so every + test starts from an untouched baseline. Starting sizes are set per VM in + the template and mirrored in the ``baselines`` passed to the helpers. + + Recording these tests leaves the VMs resized, and the tests assert the + template's starting sizes. After every recording run, reset the VMs before + recording again by re-running provision from the repo root: + + bash tools/c7n_azure/tests_azure/templates/provision.sh vm-resize + + This resizes each VM back to its starting size without recreating it. + """ + + resource_group = 'test_vm-resize' + + def _get_vm(self, vm_name): + client = local_session(Session).client('azure.mgmt.compute.ComputeManagementClient') + return client.virtual_machines.get(self.resource_group, vm_name) + + def _wait_for_vm_size(self, vm_name, vm_size): + for _ in range(40): + vm = self._get_vm(vm_name) + if (vm.hardware_profile.vm_size == vm_size and + vm.provisioning_state == 'Succeeded'): + return vm + self.sleep_in_live_mode(15) + raise AssertionError('%s never reached size %s' % (vm_name, vm_size)) + + def _name_filter(self, sizes): + return [{'type': 'value', + 'key': 'name', + 'op': 'in', + 'value': sorted(sizes)}] + + def _discover_vms(self, sizes): + """Resolve the VMs and capture their baseline; sizes is {vm_name: starting_size}.""" + p = self.load_policy({ + 'name': 'test-azure-vm-resize-discover', + 'resource': 'azure.vm', + 'filters': self._name_filter(sizes), + }) + resources = p.run() + assert sorted(r['name'] for r in resources) == sorted(sizes) + assert {r['resourceGroup'].lower() for r in resources} == {self.resource_group} + + baselines = {} + for vm_name, vm_size in sizes.items(): + vm = self._get_vm(vm_name) + assert vm.hardware_profile.vm_size == vm_size + assert vm.tags == {'testtag': 'testvalue'} + baselines[vm_name] = vm + return baselines + + def _run_resize(self, sizes, action, expected): + """Run the resize action; expected is {vm_name: requested_size} for VMs it should update.""" + p = self.load_policy({ + 'name': 'test-azure-vm-resize', + 'resource': 'azure.vm', + 'filters': self._name_filter(sizes), + 'actions': [action] + }) + operations = type(local_session(Session).client( + 'azure.mgmt.compute.ComputeManagementClient').virtual_machines) + begin_update = operations.begin_update + pollers = [] + requested = {} + + def track_begin_update(client, resource_group, vm_name, parameters, *args, **kwargs): + requested[vm_name] = parameters.hardware_profile.vm_size + poller = begin_update(client, resource_group, vm_name, parameters, *args, **kwargs) + pollers.append(poller) + return poller + + with patch.object(operations, 'begin_update', track_begin_update): + resources = p.run() + assert {r['name']: r['properties']['hardwareProfile']['vmSize'] + for r in resources} == sizes + # The cassette doesn't record request bodies, so on playback this is + # the only check that each update asked for the right size. + assert requested == expected + + # The action doesn't wait on its resize operations. Wait here so that, + # while recording, each poller's requests land in this test's cassette + # rather than the next test's. Playback disables polling, so this + # returns immediately. + for poller in pollers: + poller.wait() + + def _assert_resized(self, baseline, vm_size): + vm = self._wait_for_vm_size(baseline.name, vm_size) + assert vm.tags == baseline.tags + assert vm.os_profile.computer_name == baseline.os_profile.computer_name + + @arm_template('vm-resize.json') + @strict_cassette('test_resize') + def test_resize(self): + sizes = {'cctestvmresize-vmsize': 'Standard_B1s'} + baselines = self._discover_vms(sizes) + self._run_resize(sizes, {'type': 'resize', 'vmSize': 'Standard_B1ms'}, + {'cctestvmresize-vmsize': 'Standard_B1ms'}) + self._assert_resized(baselines['cctestvmresize-vmsize'], 'Standard_B1ms') + + @arm_template('vm-resize.json') + @strict_cassette('test_resize_type_map') + def test_resize_type_map(self): + # One policy over VMs of different sizes: each mapped VM gets its own + # target, and a VM whose size is not in the map (no default) is skipped. + sizes = { + 'cctestvmresize-map-b1s': 'Standard_B1s', + 'cctestvmresize-map-b1ms': 'Standard_B1ms', + 'cctestvmresize-map-unmapped': 'Standard_B2s', + } + baselines = self._discover_vms(sizes) + self._run_resize(sizes, { + 'type': 'resize', + 'type-map': { + # matched case-insensitively against the current size + 'standard_b1s': 'Standard_B1ms', + 'Standard_B1ms': 'Standard_B1s'}}, { + 'cctestvmresize-map-b1s': 'Standard_B1ms', + 'cctestvmresize-map-b1ms': 'Standard_B1s', + }) + + self._assert_resized(baselines['cctestvmresize-map-b1s'], 'Standard_B1ms') + self._assert_resized(baselines['cctestvmresize-map-b1ms'], 'Standard_B1s') + + unmapped = self._get_vm('cctestvmresize-map-unmapped') + assert unmapped.hardware_profile.vm_size == 'Standard_B2s' + assert unmapped.provisioning_state == 'Succeeded' + + @arm_template('vm-resize.json') + @strict_cassette('test_resize_type_map_default') + def test_resize_type_map_default(self): + sizes = {'cctestvmresize-default': 'Standard_B1s'} + baselines = self._discover_vms(sizes) + self._run_resize(sizes, { + 'type': 'resize', + 'type-map': {'Standard_B2s': 'Standard_B1s'}, + 'default': 'Standard_B1ms'}, + {'cctestvmresize-default': 'Standard_B1ms'}) + self._assert_resized(baselines['cctestvmresize-default'], 'Standard_B1ms') + + def test_resize_logs_result_per_vm(self): + with self.sign_out_patch(): + p = self.load_policy({ + 'name': 'test-azure-vm-resize', + 'resource': 'azure.vm', + 'actions': [{ + 'type': 'resize', + 'type-map': { + 'Standard_B1s': 'Standard_B1ms', + 'Standard_B1ms': 'standard_b1ms'}}] + }, validate=True) + action = p.resource_manager.actions[0] + resources = [ + {'name': name, 'resourceGroup': 'rg', + 'properties': {'hardwareProfile': {'vmSize': size}}} + for name, size in ( + ('vm-mapped', 'Standard_B1s'), + ('vm-unmapped', 'Standard_B2s'), + ('vm-same-size', 'Standard_B1ms'))] + + with patch.object(p.resource_manager, 'get_client') as get_client, \ + patch.object(action, 'log') as log: + action._process_resources(resources, None) + + begin_update = get_client.return_value.virtual_machines.begin_update + assert begin_update.call_count == 1 + assert begin_update.call_args.args[:2] == ('rg', 'vm-mapped') + assert [call.args[0] for call in log.info.call_args_list] == [ + "Action 'resize' modified 'vm-mapped' in resource group 'rg'. " + "resized from Standard_B1s to Standard_B1ms", + "Action 'resize' modified 'vm-unmapped' in resource group 'rg'. " + "not resized, no target size for Standard_B2s", + "Action 'resize' modified 'vm-same-size' in resource group 'rg'. " + "not resized, already Standard_B1ms", + ] + + def test_resize_schema_validation(self): + with self.sign_out_patch(): + for action in ( + {'type': 'resize', 'type-map': {'Standard_A1_v2': 'Standard_A2_v2'}}, + {'type': 'resize', 'default': 'Standard_A2_v2'}, + {'type': 'resize', 'type-map': {'Standard_A1_v2': 'Standard_A2_v2'}, + 'default': 'Standard_B2s'}, + ): + assert self.load_policy({ + 'name': 'test-azure-vm-resize', + 'resource': 'azure.vm', + 'actions': [action] + }, validate=True) + + for action in ( + {'type': 'resize'}, + {'type': 'resize', 'vmSize': 'Standard_A2_v2', 'default': 'Standard_B2s'}, + {'type': 'resize', 'vmSize': 'Standard_A2_v2', + 'type-map': {'Standard_A1_v2': 'Standard_A2_v2'}}, + {'type': 'resize', 'type-map': {'Standard_A1_v2': 2}}, + # present but empty: would pass validation and resize nothing + {'type': 'resize', 'vmSize': ''}, + {'type': 'resize', 'default': ''}, + {'type': 'resize', 'type-map': {}}, + {'type': 'resize', 'type-map': {'Standard_A1_v2': ''}}, + ): + with pytest.raises((PolicyValidationError, ValidationError)): + self.load_policy({ + 'name': 'test-azure-vm-resize', + 'resource': 'azure.vm', + 'actions': [action] + }, validate=True) From 0a3490c593cab1f25cf21fa2f8d5d4753953d127 Mon Sep 17 00:00:00 2001 From: Vidit Shah <80155713+proton0210@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:57:49 +0530 Subject: [PATCH 44/65] aws - account - add set-access-analyzer action (#11178) --- c7n/resources/account.py | 141 ++++++- .../access-analyzer.CreateAnalyzer_1.json | 7 + .../access-analyzer.ListAnalyzers_1.json | 7 + .../access-analyzer.ListAnalyzers_2.json | 25 ++ .../iam.ListAccountAliases_1.json | 8 + .../iam.ListAccountAliases_2.json | 8 + tests/test_account_access_analyzer.py | 391 ++++++++++++++++++ 7 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 tests/data/placebo/test_create_access_analyzer/access-analyzer.CreateAnalyzer_1.json create mode 100644 tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_1.json create mode 100644 tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_2.json create mode 100644 tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_1.json create mode 100644 tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_2.json create mode 100644 tests/test_account_access_analyzer.py diff --git a/c7n/resources/account.py b/c7n/resources/account.py index 1937b6711de..7bee2a6d70d 100644 --- a/c7n/resources/account.py +++ b/c7n/resources/account.py @@ -12,7 +12,7 @@ from dateutil.tz import tzutc from c7n.actions import ActionRegistry, BaseAction -from c7n.exceptions import PolicyValidationError +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from c7n.filters import Filter, FilterRegistry, ValueFilter from c7n.filters.kms import KmsRelatedFilter from c7n.filters.multiattr import MultiAttrFilter @@ -625,6 +625,145 @@ def process(self, resources, event=None): return matched_analyzers and resources or [] +@actions.register('set-access-analyzer') +class SetAccessAnalyzer(BaseAction): + """Enable or delete an external access analyzer in the policy's account and region. + + ``enabled`` defaults to ``true`` and ensures an analyzer is present. + ``analyzer-type`` defaults to ``ACCOUNT`` and also supports ``ORGANIZATION``. + Organization analyzers require the management or delegated administrator + account, with trusted access to IAM Access Analyzer already enabled. + + When enabled, an existing analyzer of the requested type in the region is left unchanged, + regardless of its name, including its tags and archive rules. Non-active + analyzers are logged with their status and status reason; they are not + replaced. When enabled, the configured name is used only when creating an analyzer. + When creation is needed, a name already used by a different type raises an error. + This action does not create unused-access or internal-access analyzers. + ``archive-rules`` accepts the ``ruleName`` and ``filter`` fields from the + Access Analyzer ``CreateAnalyzer`` API. + + With ``enabled: false``, only the analyzer with the configured ``analyzer-name`` + is deleted, after checking that its type matches ``analyzer-type``. A different + type raises an error; a missing analyzer is a no-op. Other analyzers are left + unchanged. Deleting an analyzer permanently deletes all of its findings and + cannot be undone. ``tags`` and ``archive-rules`` apply only to creation. + This action does not update existing analyzers; AWS ``UpdateAnalyzer`` does + not support external access analyzers. + + IAM Access Analyzer creates its service-linked role automatically when needed. + Creation therefore needs ``iam:CreateServiceLinkedRole`` for + ``access-analyzer.amazonaws.com``, ``access-analyzer:ListAnalyzers`` and + ``access-analyzer:CreateAnalyzer``, plus ``access-analyzer:TagResource`` when + tags are supplied. Deletion needs only ``access-analyzer:ListAnalyzers`` and + ``access-analyzer:DeleteAnalyzer``. + + :example: + + .. code-block:: yaml + + policies: + - name: enable-missing-access-analyzer + resource: aws.account + filters: + - not: + - type: access-analyzer + key: "[type, status]" + value: [ACCOUNT, ACTIVE] + actions: + - type: set-access-analyzer + enabled: true + analyzer-name: custodian-external-access + analyzer-type: ACCOUNT + tags: + Owner: Security + + - name: delete-named-access-analyzer + resource: aws.account + actions: + - type: set-access-analyzer + enabled: false + analyzer-name: custodian-external-access + analyzer-type: ACCOUNT + """ + + schema = type_schema( + 'set-access-analyzer', required=['analyzer-name'], **{ + 'enabled': {'type': 'boolean', 'default': True}, + 'analyzer-name': {'type': 'string', 'minLength': 1, 'maxLength': 255, + 'pattern': '^[A-Za-z][A-Za-z0-9_.-]*$'}, + 'analyzer-type': {'enum': ['ACCOUNT', 'ORGANIZATION']}, + 'archive-rules': {'type': 'array', 'items': {'type': 'object'}}, + 'tags': {'type': 'object', 'additionalProperties': {'type': 'string'}}, + }) + permissions = ( + 'access-analyzer:ListAnalyzers', 'access-analyzer:CreateAnalyzer', + 'iam:CreateServiceLinkedRole') + + def get_permissions(self): + if not self.data.get('enabled', True): + return ('access-analyzer:ListAnalyzers', 'access-analyzer:DeleteAnalyzer') + if self.data.get('tags'): + return self.permissions + ('access-analyzer:TagResource',) + return self.permissions + + def get_params(self): + params = { + 'analyzerName': self.data['analyzer-name'], + 'type': self.data.get('analyzer-type', 'ACCOUNT'), + } + for key, api_key in (('archive-rules', 'archiveRules'), ('tags', 'tags')): + if key in self.data: + params[api_key] = self.data[key] + return params + + def validate(self): + if self.data.get('enabled', True): + shape_validate(self.get_params(), 'CreateAnalyzerRequest', 'accessanalyzer') + return self + + def process(self, resources): + client = local_session(self.manager.session_factory).client('accessanalyzer') + params = self.get_params() + analyzers = [] + list_params = {} + while True: + response = self.manager.retry(client.list_analyzers, **list_params) + analyzers.extend(response['analyzers']) + if not response.get('nextToken'): + break + list_params['nextToken'] = response['nextToken'] + if not self.data.get('enabled', True): + for analyzer in analyzers: + if analyzer['name'] != params['analyzerName']: + continue + if analyzer['type'] != params['type']: + raise PolicyExecutionError( + "Access analyzer %s already has type %s, requested %s" % ( + params['analyzerName'], analyzer['type'], params['type'])) + self.manager.retry( + client.delete_analyzer, analyzerName=params['analyzerName'], + ignore_err_codes=('ResourceNotFoundException',)) + return + return + existing = [a for a in analyzers if a['type'] == params['type']] + if existing: + for analyzer in existing: + log = self.log.info if analyzer['status'] == 'ACTIVE' else self.log.warning + log( + "Access analyzer %s of type %s already exists with status %s " + "(reason: %s); skipping creation", + analyzer['name'], analyzer['type'], analyzer['status'], + analyzer.get('statusReason', {}).get('code', 'unspecified')) + return + for analyzer in analyzers: + if analyzer['name'] == params['analyzerName']: + raise PolicyExecutionError( + "Access analyzer %s already has type %s, requested %s" % ( + params['analyzerName'], analyzer['type'], params['type'])) + self.manager.retry(client.create_analyzer, **params) + + @filters.register('password-policy') class AccountPasswordPolicy(ValueFilter): """Check an account's password policy. diff --git a/tests/data/placebo/test_create_access_analyzer/access-analyzer.CreateAnalyzer_1.json b/tests/data/placebo/test_create_access_analyzer/access-analyzer.CreateAnalyzer_1.json new file mode 100644 index 00000000000..974d573ef63 --- /dev/null +++ b/tests/data/placebo/test_create_access_analyzer/access-analyzer.CreateAnalyzer_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "arn": "arn:aws:access-analyzer:us-east-1:644160558196:analyzer/custodian-external-access" + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_1.json b/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_1.json new file mode 100644 index 00000000000..d8b64b68035 --- /dev/null +++ b/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "analyzers": [] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_2.json b/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_2.json new file mode 100644 index 00000000000..a48164a9f3c --- /dev/null +++ b/tests/data/placebo/test_create_access_analyzer/access-analyzer.ListAnalyzers_2.json @@ -0,0 +1,25 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "analyzers": [ + { + "arn": "arn:aws:access-analyzer:us-east-1:644160558196:analyzer/custodian-external-access", + "name": "custodian-external-access", + "type": "ACCOUNT", + "createdAt": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 9, + "hour": 1, + "minute": 6, + "second": 41, + "microsecond": 0 + }, + "tags": {}, + "status": "ACTIVE" + } + ] + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_1.json b/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_1.json new file mode 100644 index 00000000000..384ece3f3bb --- /dev/null +++ b/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "AccountAliases": [], + "IsTruncated": false, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_2.json b/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_2.json new file mode 100644 index 00000000000..384ece3f3bb --- /dev/null +++ b/tests/data/placebo/test_create_access_analyzer/iam.ListAccountAliases_2.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "AccountAliases": [], + "IsTruncated": false, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_account_access_analyzer.py b/tests/test_account_access_analyzer.py new file mode 100644 index 00000000000..a109a3ee455 --- /dev/null +++ b/tests/test_account_access_analyzer.py @@ -0,0 +1,391 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import datetime +import logging +from unittest import mock + +import boto3 +from botocore.exceptions import ClientError +from botocore.stub import Stubber +import pytest + +from c7n.exceptions import PolicyExecutionError, PolicyValidationError + + +ANALYZER_NAME = 'custodian-external-access' +ANALYZER_ARN = 'arn:aws:access-analyzer:us-east-1:123456789012:analyzer/' + ANALYZER_NAME + + +@pytest.fixture +def analyzer_session(): + session = boto3.Session( + region_name='us-east-1', aws_access_key_id='test', aws_secret_access_key='test') + clients = {name: session.client(name) for name in ('accessanalyzer', 'iam')} + stub_session = mock.Mock(client=lambda name: clients[name]) + + def factory(): + return stub_session + with Stubber(clients['accessanalyzer']) as analyzer, Stubber(clients['iam']) as iam: + yield factory, analyzer, iam + analyzer.assert_no_pending_responses() + iam.assert_no_pending_responses() + + +def analyzer_summary(analyzer_type='ACCOUNT', name=ANALYZER_NAME, status='ACTIVE'): + return { + 'arn': ANALYZER_ARN.rsplit('/', 1)[0] + '/' + name, + 'name': name, + 'type': analyzer_type, + 'status': status, + 'createdAt': datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), + } + + +def load_analyzer_policy(test, factory=None, **options): + return test.load_policy({ + 'name': 'set-access-analyzer', + 'resource': 'aws.account', + 'actions': [{ + 'type': 'set-access-analyzer', + 'analyzer-name': ANALYZER_NAME, + **options, + }], + }, session_factory=factory) + + +@pytest.mark.parametrize('options', [{}, {'enabled': True}]) +def test_set_access_analyzer_repeated_run(test, analyzer_session, options): + factory, analyzer, _ = analyzer_session + analyzer.add_response('list_analyzers', {'analyzers': []}, {}) + analyzer.add_response('create_analyzer', {'arn': ANALYZER_ARN}, { + 'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'}) + analyzer.add_response('list_analyzers', {'analyzers': [analyzer_summary()]}, {}) + policy = load_analyzer_policy(test, factory, **options) + action = policy.resource_manager.actions[0] + original = dict(action.data) + + action.process([{'account_id': '123456789012'}]) + action.process([{'account_id': '123456789012'}]) + + assert action.data == original + + +def test_set_access_analyzer_options(test, analyzer_session): + factory, analyzer, _ = analyzer_session + rules = [{'ruleName': 'trusted-account', 'filter': { + 'principal.AWS': {'eq': ['123456789012']}}}] + tags = {'Owner': 'Security'} + analyzer.add_response('list_analyzers', {'analyzers': []}, {}) + analyzer.add_response('create_analyzer', {'arn': ANALYZER_ARN}, { + 'analyzerName': ANALYZER_NAME, 'type': 'ORGANIZATION', + 'archiveRules': rules, 'tags': tags}) + policy = load_analyzer_policy(test, factory, **{ + 'analyzer-type': 'ORGANIZATION', 'archive-rules': rules, 'tags': tags}) + + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + + +def test_set_access_analyzer_type_conflict(test, analyzer_session): + factory, analyzer, _ = analyzer_session + analyzer.add_response( + 'list_analyzers', {'analyzers': [analyzer_summary('ORGANIZATION')]}, {}) + policy = load_analyzer_policy(test, factory) + + with pytest.raises(PolicyExecutionError, match='ORGANIZATION.*ACCOUNT'): + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('operation,code', [ + ('list_analyzers', 'AccessDeniedException'), + ('create_analyzer', 'AccessDeniedException'), + ('create_analyzer', 'ServiceQuotaExceededException'), + ('create_analyzer', 'ConflictException'), +]) +def test_set_access_analyzer_api_error(test, analyzer_session, operation, code): + factory, analyzer, _ = analyzer_session + expected = {} + if operation == 'create_analyzer': + analyzer.add_response('list_analyzers', {'analyzers': []}, {}) + expected = {'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'} + analyzer.add_client_error(operation, service_error_code=code, expected_params=expected) + policy = load_analyzer_policy(test, factory) + + with pytest.raises(ClientError) as error: + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + assert error.value.response['Error']['Code'] == code + + +@pytest.mark.parametrize('options', [ + {'enabled': 'false'}, + {'enabled': 0}, + {'analyzer-name': ''}, + {'analyzer-name': 'invalid/name'}, + {'analyzer-name': 'a' * 256}, + {'analyzer-type': 'ACCOUNT_UNUSED_ACCESS'}, + {'tags': {'Owner': 123}}, + {'archive-rules': [{'ruleName': 'missing-filter'}]}, + {'archive-rules': [{'ruleName': 'invalid', 'filter': {'isPublic': {'exists': 'yes'}}}]}, + {'archive-rules': [{'ruleName': 'invalid', 'filter': {}, 'unknown': True}]}, +]) +def test_set_access_analyzer_invalid_options(test, options): + with pytest.raises(PolicyValidationError): + load_analyzer_policy(test, **options) + + +@pytest.mark.parametrize('enabled', [True, False]) +def test_set_access_analyzer_requires_name(test, enabled): + with pytest.raises(PolicyValidationError): + test.load_policy({ + 'name': 'missing-analyzer-name', 'resource': 'aws.account', + 'actions': [{'type': 'set-access-analyzer', 'enabled': enabled}], + }) + + +@pytest.mark.parametrize('tags', [None, {'Owner': 'Security'}]) +def test_set_access_analyzer_permissions(test, tags): + policy = load_analyzer_policy(test, **({'tags': tags} if tags else {})) + permissions = policy.resource_manager.actions[0].get_permissions() + assert set(permissions) == { + 'access-analyzer:ListAnalyzers', 'access-analyzer:CreateAnalyzer', + 'iam:CreateServiceLinkedRole', + } | ({'access-analyzer:TagResource'} if tags else set()) + + +@pytest.mark.parametrize('existing_type', [None, 'ACCOUNT_UNUSED_ACCESS']) +def test_set_access_analyzer_with_account_filter(test, analyzer_session, existing_type): + factory, analyzer, iam = analyzer_session + for _ in range(2): + iam.add_response('list_account_aliases', {'AccountAliases': []}, {}) + existing = [{ + **analyzer_summary(existing_type), + 'name': 'other-unused-access', + 'arn': 'arn:aws:access-analyzer:us-east-1:123456789012:analyzer/other-unused-access', + }] if existing_type else [] + analyzer.add_response('list_analyzers', {'analyzers': existing}, {}) + analyzer.add_response('list_analyzers', {'analyzers': []}, {}) + analyzer.add_response('create_analyzer', {'arn': ANALYZER_ARN}, { + 'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'}) + analyzer.add_response('list_analyzers', {'analyzers': [analyzer_summary()]}, {}) + policy = test.load_policy({ + 'name': 'enable-missing-access-analyzer', + 'resource': 'aws.account', + 'filters': [{'not': [{ + 'type': 'access-analyzer', 'key': '[type, status]', + 'value': ['ACCOUNT', 'ACTIVE'], + }]}], + 'actions': [{ + 'type': 'set-access-analyzer', 'analyzer-name': ANALYZER_NAME, + }], + }, session_factory=factory) + + assert len(policy.run()) == 1 + assert policy.run() == [] + + +@pytest.mark.parametrize('operation', ['list_analyzers', 'create_analyzer']) +def test_set_access_analyzer_retries_throttling( + test, analyzer_session, monkeypatch, operation): + factory, analyzer, _ = analyzer_session + monkeypatch.setattr('c7n.utils.time.sleep', lambda _: None) + expected = {} + if operation == 'create_analyzer': + analyzer.add_response('list_analyzers', {'analyzers': []}, {}) + expected = {'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'} + analyzer.add_client_error( + operation, service_error_code='ThrottlingException', + http_status_code=429, expected_params=expected) + response = {'analyzers': [analyzer_summary()]} if operation == 'list_analyzers' else { + 'arn': ANALYZER_ARN} + analyzer.add_response(operation, response, expected) + policy = load_analyzer_policy(test, factory) + + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('analyzer_type', ['ACCOUNT', 'ORGANIZATION']) +@pytest.mark.parametrize('status', ['ACTIVE', 'CREATING', 'DISABLED', 'FAILED']) +def test_set_access_analyzer_existing_in_region( + test, analyzer_session, caplog, analyzer_type, status): + factory, analyzer, _ = analyzer_session + existing = analyzer_summary(analyzer_type, name='existing-external-access', status=status) + if status == 'FAILED': + existing['statusReason'] = {'code': 'INTERNAL_ERROR'} + analyzer.add_response('list_analyzers', {'analyzers': [existing]}, {}) + policy = load_analyzer_policy(test, factory, **{'analyzer-type': analyzer_type}) + + with caplog.at_level(logging.INFO, logger='custodian.actions'): + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + + assert 'existing-external-access' in caplog.text + assert analyzer_type in caplog.text + assert 'status ' + status in caplog.text + assert 'skipping creation' in caplog.text + assert caplog.records[-1].levelno == ( + logging.INFO if status == 'ACTIVE' else logging.WARNING) + assert ('INTERNAL_ERROR' if status == 'FAILED' else 'unspecified') in caplog.text + + +@pytest.mark.parametrize('existing_type', [ + 'ORGANIZATION', 'ACCOUNT_UNUSED_ACCESS', 'ACCOUNT_INTERNAL_ACCESS']) +def test_set_access_analyzer_other_types(test, analyzer_session, existing_type): + factory, analyzer, _ = analyzer_session + analyzer.add_response('list_analyzers', { + 'analyzers': [analyzer_summary(existing_type, name='other-analyzer')]}, {}) + analyzer.add_response('create_analyzer', {'arn': ANALYZER_ARN}, { + 'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'}) + + load_analyzer_policy(test, factory).resource_manager.actions[0].process( + [{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('existing_type', ['ACCOUNT', 'ACCOUNT_UNUSED_ACCESS']) +def test_set_access_analyzer_paginated(test, analyzer_session, existing_type): + factory, analyzer, _ = analyzer_session + analyzer.add_response('list_analyzers', {'analyzers': [], 'nextToken': 'next-page'}, {}) + analyzer.add_response('list_analyzers', { + 'analyzers': [analyzer_summary(existing_type, name='other-analyzer')]}, { + 'nextToken': 'next-page'}) + if existing_type != 'ACCOUNT': + analyzer.add_response('create_analyzer', {'arn': ANALYZER_ARN}, { + 'analyzerName': ANALYZER_NAME, 'type': 'ACCOUNT'}) + + load_analyzer_policy(test, factory).resource_manager.actions[0].process( + [{'account_id': '123456789012'}]) + + +def test_set_access_analyzer_recorded(test, caplog): + factory = test.replay_flight_data('test_create_access_analyzer', region='us-east-1') + policy = load_analyzer_policy(test, factory) + + # Count calls as placebo playback can reuse a response if called again. + with mock.patch.object(policy.resource_manager.actions[0].manager, 'retry', + wraps=policy.resource_manager.retry) as retry: + with caplog.at_level(logging.INFO, logger='custodian.actions'): + assert len(policy.run()) == 1 + assert len(policy.run()) == 1 + operations = [call.args[0].__name__ for call in retry.call_args_list] + + assert operations.count('create_analyzer') == 1 + assert operations.count('list_analyzers') == 2 + assert 'skipping creation' in caplog.text + + +@pytest.mark.parametrize('analyzer_type', ['ACCOUNT', 'ORGANIZATION']) +@pytest.mark.parametrize('status', ['ACTIVE', 'CREATING', 'DISABLED', 'FAILED']) +def test_set_access_analyzer_delete_named( + test, analyzer_session, analyzer_type, status): + factory, analyzer, _ = analyzer_session + # Even a differently named analyzer of the requested type must be preserved. + unrelated = [analyzer_summary(analyzer_type, name='other-external-access'), + analyzer_summary('ACCOUNT_UNUSED_ACCESS', name='other-unused-access')] + analyzer.add_response('list_analyzers', { + 'analyzers': unrelated, 'nextToken': 'next-page'}, {}) + analyzer.add_response('list_analyzers', { + 'analyzers': [analyzer_summary(analyzer_type, status=status)]}, { + 'nextToken': 'next-page'}) + analyzer.add_response('delete_analyzer', {}, {'analyzerName': ANALYZER_NAME}) + analyzer.add_response('list_analyzers', {'analyzers': unrelated}, {}) + policy = load_analyzer_policy(test, factory, **{ + 'enabled': False, 'analyzer-type': analyzer_type, + 'tags': {'Owner': 'Security'}, + 'archive-rules': [{'ruleName': 'missing-filter'}], + }) + action = policy.resource_manager.actions[0] + original = dict(action.data) + + action.process([{'account_id': '123456789012'}]) + action.process([{'account_id': '123456789012'}]) + + assert action.data == original + + +@pytest.mark.parametrize('existing_type', [None, 'ACCOUNT', 'ORGANIZATION']) +def test_set_access_analyzer_delete_missing(test, analyzer_session, existing_type): + factory, analyzer, _ = analyzer_session + unrelated = [analyzer_summary(existing_type, name='other-analyzer')] if existing_type else [] + analyzer.add_response('list_analyzers', {'analyzers': unrelated}, {}) + + load_analyzer_policy(test, factory, enabled=False).resource_manager.actions[0].process( + [{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('requested_type,existing_type', [ + ('ACCOUNT', 'ORGANIZATION'), + ('ORGANIZATION', 'ACCOUNT'), + ('ACCOUNT', 'ACCOUNT_UNUSED_ACCESS'), + ('ACCOUNT', 'ACCOUNT_INTERNAL_ACCESS'), +]) +def test_set_access_analyzer_delete_type_conflict( + test, analyzer_session, requested_type, existing_type): + factory, analyzer, _ = analyzer_session + analyzer.add_response('list_analyzers', {'analyzers': [ + analyzer_summary(requested_type, name='other-external-access'), + analyzer_summary(existing_type), + ]}, {}) + policy = load_analyzer_policy(test, factory, **{ + 'enabled': False, 'analyzer-type': requested_type}) + + with pytest.raises(PolicyExecutionError, match=existing_type + '.*' + requested_type): + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('tags', [None, {'Owner': 'Security'}]) +def test_set_access_analyzer_delete_permissions(test, tags): + policy = load_analyzer_policy(test, enabled=False, **({'tags': tags} if tags else {})) + + assert set(policy.resource_manager.actions[0].get_permissions()) == { + 'access-analyzer:ListAnalyzers', 'access-analyzer:DeleteAnalyzer'} + + +@pytest.mark.parametrize('operation,code', [ + ('list_analyzers', 'AccessDeniedException'), + ('delete_analyzer', 'AccessDeniedException'), + ('delete_analyzer', 'InternalServerException'), + ('delete_analyzer', 'ValidationException'), +]) +def test_set_access_analyzer_delete_api_error( + test, analyzer_session, monkeypatch, operation, code): + factory, analyzer, _ = analyzer_session + monkeypatch.setattr('c7n.utils.time.sleep', lambda _: None) + expected = {} + if operation == 'delete_analyzer': + analyzer.add_response('list_analyzers', {'analyzers': [analyzer_summary()]}, {}) + expected = {'analyzerName': ANALYZER_NAME} + # Transient errors must still propagate after the manager exhausts its retries. + for _ in range(8 if code == 'InternalServerException' else 1): + analyzer.add_client_error(operation, service_error_code=code, expected_params=expected) + policy = load_analyzer_policy(test, factory, enabled=False) + + with pytest.raises(ClientError) as error: + policy.resource_manager.actions[0].process([{'account_id': '123456789012'}]) + assert error.value.response['Error']['Code'] == code + + +def test_set_access_analyzer_delete_disappeared(test, analyzer_session): + factory, analyzer, _ = analyzer_session + analyzer.add_response('list_analyzers', {'analyzers': [analyzer_summary()]}, {}) + analyzer.add_client_error( + 'delete_analyzer', service_error_code='ResourceNotFoundException', + http_status_code=404, expected_params={'analyzerName': ANALYZER_NAME}) + + load_analyzer_policy(test, factory, enabled=False).resource_manager.actions[0].process( + [{'account_id': '123456789012'}]) + + +@pytest.mark.parametrize('operation', ['list_analyzers', 'delete_analyzer']) +def test_set_access_analyzer_delete_retries_throttling( + test, analyzer_session, monkeypatch, operation): + factory, analyzer, _ = analyzer_session + monkeypatch.setattr('c7n.utils.time.sleep', lambda _: None) + expected = {} + if operation == 'delete_analyzer': + analyzer.add_response('list_analyzers', {'analyzers': [analyzer_summary()]}, {}) + expected = {'analyzerName': ANALYZER_NAME} + analyzer.add_client_error( + operation, service_error_code='ThrottlingException', + http_status_code=429, expected_params=expected) + response = {'analyzers': []} if operation == 'list_analyzers' else {} + analyzer.add_response(operation, response, expected) + + load_analyzer_policy(test, factory, enabled=False).resource_manager.actions[0].process( + [{'account_id': '123456789012'}]) From bcae85945b7832f6c979221ff6638f07c270cfa3 Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Sat, 17 Oct 2026 07:29:23 +0300 Subject: [PATCH 45/65] gcp - instance - add set-ip-forwarding action (#10640) --------- Co-authored-by: Caleb Syring Co-authored-by: Kody Mullins --- tools/c7n_gcp/c7n_gcp/resources/compute.py | 60 + ...loud-custodian-aggregated-instances_1.json | 1700 +++++++++++++++++ ...-central1-a-instances-sacred-dassie_1.json | 140 ++ ...-central1-a-instances-sacred-dassie_1.json | 31 + .../gcp_instance_set_can_ip_forward/main.tf | 44 + .../tf_resources.json | 219 +++ tools/c7n_gcp/tests/test_compute.py | 64 +- 7 files changed, 2257 insertions(+), 1 deletion(-) create mode 100644 tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-aggregated-instances_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/put-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json create mode 100644 tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/compute.py b/tools/c7n_gcp/c7n_gcp/resources/compute.py index 4737f3c2cf6..1c8dc788b17 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/compute.py +++ b/tools/c7n_gcp/c7n_gcp/resources/compute.py @@ -371,6 +371,66 @@ def get_resource_params(self, model, resource): return params +@Instance.action_registry.register('set-ip-forwarding') +class InstanceSetIpForwarding(InstanceAction): + """Enable or disable IP forwarding on a Compute Engine instance. + + The ``enabled`` parameter is required and must be a boolean. + + :example: + + Enable IP forwarding on all instances that have it disabled: + + .. code-block:: yaml + + policies: + - name: gcp-instance-enable-ip-forward + resource: gcp.instance + filters: + - type: value + key: canIpForward + op: eq + value: false + actions: + - type: set-ip-forwarding + enabled: true + + Disable IP forwarding on instances that should not have it: + + .. code-block:: yaml + + policies: + - name: gcp-instance-disable-ip-forward + resource: gcp.instance + filters: + - type: value + key: canIpForward + op: eq + value: true + actions: + - type: set-ip-forwarding + enabled: false + + """ + schema = type_schema( + 'set-ip-forwarding', + required=['enabled'], + enabled={'type': 'boolean'}) + method_spec = {'op': 'update'} + permissions = ('compute.instances.update',) + + def get_resource_params(self, model, resource): + params = super().get_resource_params(model, resource) + body = dict(resource) + body['canIpForward'] = self.data['enabled'] + params.update({ + 'minimalAction': 'REFRESH', + 'mostDisruptiveAllowedAction': 'REFRESH', + 'body': body, + }) + return params + + @resources.register('image') class Image(QueryResourceManager): diff --git a/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-aggregated-instances_1.json b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-aggregated-instances_1.json new file mode 100644 index 00000000000..f20a29ca538 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-aggregated-instances_1.json @@ -0,0 +1,1700 @@ +{ + "headers": { + "etag": "TY0ZiAs-3YLe9cf4OdyGoqodEDc=/DqHKgMFONZAv73PeH382cG6dcpg=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 13 Mar 2026 15:36:41 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "57010", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/instances?alt=json" + }, + "body": { + "kind": "compute#instanceAggregatedList", + "id": "projects/cloud-custodian/aggregated/instances", + "items": { + "zones/us-central1-a": { + "instances": [ + { + "kind": "compute#instance", + "id": "7368592281459855388", + "creationTimestamp": "2026-03-13T08:36:19.675-07:00", + "name": "sacred-dassie", + "tags": { + "fingerprint": "42WmSpB8rSM=" + }, + "machineType": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/machineTypes/e2-micro", + "status": "RUNNING", + "zone": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a", + "canIpForward": false, + "networkInterfaces": [ + { + "kind": "compute#networkInterface", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "subnetwork": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/sacred-dassie-subnet", + "networkIP": "10.2.0.2", + "name": "nic0", + "accessConfigs": [ + { + "kind": "compute#accessConfig", + "type": "ONE_TO_ONE_NAT", + "name": "external-nat", + "natIP": "35.225.226.220", + "networkTier": "PREMIUM" + } + ], + "fingerprint": "s-mVbaV9s2M=", + "stackType": "IPV4_ONLY" + } + ], + "disks": [ + { + "kind": "compute#attachedDisk", + "type": "PERSISTENT", + "mode": "READ_WRITE", + "source": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/disks/sacred-dassie", + "deviceName": "persistent-disk-0", + "index": 0, + "boot": true, + "autoDelete": true, + "licenses": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/licenses/ubuntu-2204-lts" + ], + "interface": "SCSI", + "guestOsFeatures": [ + { + "type": "VIRTIO_SCSI_MULTIQUEUE" + }, + { + "type": "SEV_CAPABLE" + }, + { + "type": "SEV_SNP_CAPABLE" + }, + { + "type": "SEV_LIVE_MIGRATABLE" + }, + { + "type": "SEV_LIVE_MIGRATABLE_V2" + }, + { + "type": "IDPF" + }, + { + "type": "TDX_CAPABLE" + }, + { + "type": "UEFI_COMPATIBLE" + }, + { + "type": "GVNIC" + } + ], + "diskSizeGb": "10", + "shieldedInstanceInitialState": { + "dbxs": [ + { + "content": "2gcDBhMRFQAAAAAAAAAAABENAAAAAvEOndKvSt9o7kmKqTR9N1ZlpzCCDPUCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggsIMIIFGDCCBACgAwIBAgITMwAAABNryScg3e1ZiAAAAAAAEzANBgkqhkiG9w0BAQsFADCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEqMCgGA1UEAxMhTWljcm9zb2Z0IENvcnBvcmF0aW9uIEtFSyBDQSAyMDExMB4XDTE2MDEwNjE4MzQxNVoXDTE3MDQwNjE4MzQxNVowgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIxMDAuBgNVBAMTJ01pY3Jvc29mdCBXaW5kb3dzIFVFRkkgS2V5IEV4Y2hhbmdlIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXiCkZgbboTnVZnS1h_JbnlcVst9wtFK8NQjTpeB9wirml3h-fzi8vzki0hSNBD2Dg49lGEvs4egyowmTsLu1TnBUH1f_Hi8Noa7fKXV6F93qYrTPajx5v9L7NedplWnMEPsRvJrQdrysTZwtoXMLYDhc8bQHI5nlJDfgqrB8JiC4A3vL9i19lkQOTq4PZb5AcVcE0wlG7lR_btoQN0g5B4_7pI2S_9mU1PXr1NBSEl48Kl4cJwO2GyvOVvxQ6wUSFTExmCBKrT3LnPU5lZY68n3MpZ5VY4skhrEt2dyf5bZNzkYTTouxC0n37OrMbGGq3tpv7JDD6E_Rfqua3dXYECAwEAAaOCAXIwggFuMBQGA1UdJQQNMAsGCSsGAQQBgjdPATAdBgNVHQ4EFgQUVsJIppTfox2XYoAJRIlnxAUOy2owUQYDVR0RBEowSKRGMEQxDTALBgNVBAsTBE1PUFIxMzAxBgNVBAUTKjMxNjMxKzJjNDU2Y2JjLTA1NDItNDdkOS05OWU1LWQzOWI4MTVjNTczZTAfBgNVHSMEGDAWgBRi_EPNoD6ky2cS0lvZVax7zLaKXzBTBgNVHR8ETDBKMEigRqBEhkJodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb3JLRUtDQTIwMTFfMjAxMS0wNi0yNC5jcmwwYAYIKwYBBQUHAQEEVDBSMFAGCCsGAQUFBzAChkRodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvcktFS0NBMjAxMV8yMDExLTA2LTI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGjTFLjxsKmyLESJueg0S2Cp8N7MOq2IALsitZHwfYw2jMhY9b9kmKvIdSqVna1moZ6_zJSOS_JY6HkWZr6dDJe9Lj7xiW_e4qPP-KDrCVb02vBnK4EktVjTdJpyMhxBMdXUcq1eGl6518oCkQ27tu0-WZjaWEVsEY_gpQj0ye2UA4HYUYgJlpT24oJRi7TeQ03Nebb-ZrUkbf9uxl0OVV_mg2R5FDwOc3REoRAgv5jnw6X7ha5hlRCl2cLF27TFrFIRQQT4eSM33eDiitXXpYmD13jqKeHhLVXr07QSwqvKe1o1UYokJngP0pTwoDnt2qRuLnZ71jw732dSPN9B57MIIF6DCCA9CgAwIBAgIKYQrRiAAAAAAAAzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE7MDkGA1UEAxMyTWljcm9zb2Z0IENvcnBvcmF0aW9uIFRoaXJkIFBhcnR5IE1hcmtldHBsYWNlIFJvb3QwHhcNMTEwNjI0MjA0MTI5WhcNMjYwNjI0MjA1MTI5WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEqMCgGA1UEAxMhTWljcm9zb2Z0IENvcnBvcmF0aW9uIEtFSyBDQSAyMDExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxOi1ir-tVyawJsPq5_tXekQCXQcN2krldCrmsA_sbevsf7njWmMyfBEXTw7jC6c4FZOOxvXghLGamyzn9beR1gnh4sAEqKwwHN9I8wZQmmSnUX_IhU-PIIbO_i_hn_-CwO3pzc70U2piOgtDueIl_f4F-dTEFKsR4iOJjXC3pB1N7K7lnPoWwtfBy9ToxC_lme4kiwPsjfKL6sNK-0MREgt-tUeSbNzmBInr9TME6xABKnHl-YMTPP8lCS9odkb_uk--3K1xKliq-w7SeT3km2U7zCkqn_xyWaLrrpLv9jUTgMYC7ORfzJ12ze9jksGveUCEeYd_41Ko6J17B2mPFQIDAQABo4IBTzCCAUswEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGL8Q82gPqTLZxLSW9lVrHvMtopfMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH_MB8GA1UdIwQYMBaAFEVmUkPhflgRv9ZOniNVCDs6ImqoMFwGA1UdHwRVMFMwUaBPoE2GS2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY0NvclRoaVBhck1hclJvb18yMDEwLTEwLTA1LmNybDBgBggrBgEFBQcBAQRUMFIwUAYIKwYBBQUHMAKGRGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljQ29yVGhpUGFyTWFyUm9vXzIwMTAtMTAtMDUuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQDUhIj1FJQYAsoqPPsqkhwM16DR8ehSZqjuorV1epAAqi2kdlrqebe5N2pRexBk9uFk8gJnvveoG3i9us6IWGQM1lfIGaNfBdbbxtBpzkhLMrfrXdIw9cD1uLp4B6Mr_pvbNFaE7ILKrkElcJxr6f6QD9eWH-XnlB-yKgyNS_8oKRB799d8pdF2uQXIee0PkJKcwv7fb35sD3vUwUXdNFGWOQ_lXlbYGAWW9AemQrOgd_0IGfJxVsyfhiOkh8um_Vh-1GlnFZF-gfJ_E-UNi4o8h4Tr4869Q-WtLYSTjmorWnxE-lKqgcgtHLvgUt8AEfiaPcFgsOEztaOI0WUZChrnrHykwYKHTjixLw3FFIdv_Y0uvDm25-bD4OTNJ4TvlELvKYuQRkE7gRtn2PlDWWXLDbz9AJJP9HU7p6kk_FBBQHngLU8Kaid2blLtlml7rw_3hwXQRcKtUxSBH_swBKo3NmHaSmkbNNho7dYCz2yUDNPPbCJ5rbHwvAOiRmCpxAfCIYLx_fLoeTJgv9ispSIUS8rB2EvrfT9XNbLmT3W0sGADIlOukXkd1ptBHxWGVHCy3g01D3ywNHK6l2A78HnrorIcXaIWuIfF6Rv2tZclbzif45H6inmYw2kOt6McIAWX-MoUrgDXxPPAFBB1azSgG7WZYPNcsMVXTjbSMoS_njGCAcQwggHAAgEBMIGYMIGAMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSowKAYDVQQDEyFNaWNyb3NvZnQgQ29ycG9yYXRpb24gS0VLIENBIDIwMTECEzMAAAATa8knIN3tWYgAAAAAABMwDQYJYIZIAWUDBAIBBQAwDQYJKoZIhvcNAQEBBQAEggEAhabaxRIJ7nUZ-m__mIG0lII6yD-lxoeI8S83ZKTP8Qx5h5asySWl7420eGhna7zyaVRvVVIhkjOMIfcKr29LgzQpYDqPUc8aYAdGCsZKZGmHCMjEulnq5TDK79GKinzZfb2sAWXEJ68N8oNnY7faBKjHjmmJbAEz8ufE4DijgJ_NBov2xmhTZyNHQ7pB1iCdrEUGObzdJc0Qtmh3CNOEcmH0ukd8sTHE9acBBTFHS8dvreR_sP7dXClZJbJiWAFKvQn3EjCTiYizkZ4I_5xiqjHELht_ORQKN-Hnoqnl4kcRINhZRV7JlgAQDlBJLv3OTjShRO_ZWCdcu7PtwhweiSYWxMFMUJJArKlB-TaTQyiMDgAAAAAAADAAAAC9mvp3WQMyTb1gKPTnj3hLgLTZaTG_DQL9kaYeGdFPHaRS5m2yQIyoYE1BH5Jlnwq9mvp3WQMyTb1gKPTnj3hL9S-Do_qc-9aSD3IoJNvkA0U00luFByRrO5V9rG4bznq9mvp3WQMyTb1gKPTnj3hLxdnYoYbiyC0Jr6oqb38uc4cNPmT3LE4I72d5aoQPD729mvp3WQMyTb1gKPTnj3hLNjOE0U0fLgt4FWJkhMRZrVejGO9DliZgSNBYxaGbv3a9mvp3WQMyTb1gKPTnj3hLGuyEuEtsZaUSIKm-cYGWUjAhDWLW0zxImZxrKVorCga9mvp3WQMyTb1gKPTnj3hL5spo6UFGYprwP2nC-G5r72L5MLN8b7zIeLeN-YwDNOW9mvp3WQMyTb1gKPTnj3hLw6maRg2kZKBXw1htg8719K4ItxA5ee2JMnQt8O1TDGa9mvp3WQMyTb1gKPTnj3hLWPuUGu-VollDs_tfJRCg3z_kTFjJXgq4BIcpdWirl3G9mvp3WQMyTb1gKPTnj3hLU5HDovsRIQKmqh7cJa534Z9dbwnNCe6yUJkiv81Zkuq9mvp3WQMyTb1gKPTnj3hL1iYVfh1qcYvBJKuNony7ZQcsoDp7ayV9vcu9YPZe89G9mvp3WQMyTb1gKPTnj3hL0GPsKPZ-ulPxZC2_ff8zxqMq3YafYBP-Fi4sMvHL5W29mvp3WQMyTb1gKPTnj3hLKcbrUrQ8OqGLLNjtbqhgfO88-uG6_hFldVzy5hSESkS9mvp3WQMyTb1gKPTnj3hLkPvnDmnWM0CNPhcMaDLbstIJ4CclJ9-2PUnSlXKm9Ey9mvp3WQMyTb1gKPTnj3hLB17qBgWJVIugYLL-7RDaPCDH_psXzQJrlOimg7gRUji9mvp3WQMyTb1gKPTnj3hLB-bGqFhkb7HvxnkD_iixFgEfI2f-kua-KzaZnv850J69mvp3WQMyTb1gKPTnj3hLCd9fTlESCOx4uW0S0IEl_bYDho3jn29yknhSWZtlnCa9mvp3WQMyTb1gKPTnj3hLC7tDktqseribMKSsZXUxuXv6qwT5Cw2v5fm265CgY3S9mvp3WQMyTb1gKPTnj3hLDBiTOXYt8zarPdAGpGPfcVo5z7D0kkZcYA5sa9e9iYy9mvp3WQMyTb1gKPTnj3hLDQ2-ym8p7KBvMxp9cuSISxIJf7NImDoqFKDXP08QFA-9mvp3WQMyTb1gKPTnj3hLDcnz-5mWIUjDyoM2MnWNPtT8jQsAB7lbMeZSjyrNW_y9mvp3WQMyTb1gKPTnj3hLEG-s6s_s_U4wO3T0gKCAmOLQgCuTb47HdM4h8xaGaJy9mvp3WQMyTb1gKPTnj3hLF046C1tDxqYHu9NATwU0Hj3POWJnzpT4tQ4uI6nakgy9mvp3WQMyTb1gKPTnj3hLGDM0Kf8FYu2flwM-EUjc7uUtvi5JbVQQtc_WyGTS0Q-9mvp3WQMyTb1gKPTnj3hLK5nPJkIukv42X79Lww0nCGye4Ut6b_9E-y9rkAFpmTm9mvp3WQMyTb1gKPTnj3hLK78sp7jx2R8n7lK2-ypd0Em4WiubUpxdZmIGgQSwVfi9mvp3WQMyTb1gKPTnj3hLLHPZMyW6bcvlidSkxjxbk1VZ75L78FDtUMTiCFIG8X29mvp3WQMyTb1gKPTnj3hLLnCRZ4am93NRH6cYH6sPHXC1V8YyLqkjsqjTuStRr329mvp3WQMyTb1gKPTnj3hLMGYo-lR3MFcoukpGfefQOHpU9WnTdp_OXnXsidKNFZO9mvp3WQMyTb1gKPTnj3hLNgjtuvWtD0GkFKF3er8vr15nAzRnXsOZXmk1gp4MqtK9mvp3WQMyTb1gKPTnj3hLOEHSITaNFYPXXAoC5iFgOU1sTgpnYLb2B7kDYryFWwK9mvp3WQMyTb1gKPTnj3hLP86bn98-8J1UUrD5XuSBwrfwbXQ6c3lxVY5wE2rOPnO9mvp3WQMyTb1gKPTnj3hLQ5fayoOef2MHfLUMkt9DvC0vsqj1nyb8eg5L1Nl1FpK9mvp3WQMyTb1gKPTnj3hLR8wIYSfiBpqG4Dpr7yzUEPjFWm1r2zYhaMMbLOMqWt-9mvp3WQMyTb1gKPTnj3hLUYgx_nOCtRTQPhXGISKLirZUeb0Mv6PFwdD0jZwwYTW9mvp3WQMyTb1gKPTnj3hLWulJ6ohV65PkOdvGW9ouQoUsL99nifoUZzbjw0EPK1y9mvp3WQMyTb1gKPTnj3hLax0TgHjkQYqmjet7s14GYJLPR57rjOTNEufQcsy0L2a9mvp3WQMyTb1gKPTnj3hLbIhUR43VWeKTUbgmwGy4v-8rlK01ODWHctGT-C7RyhG9mvp3WQMyTb1gKPTnj3hLbxQo_3HJ2w7Vrx8ue7_Lq2R8wmXd9bKTzbYm9Qo6eF69mvp3WQMyTb1gKPTnj3hLcfKQb9IiSX5Uo0ZiqySX_MgQIHcP9RNo6ePZv8v9Y3W9mvp3WQMyTb1gKPTnj3hLcms-tlQEajDz-D2bls4D9nDpqAbRcIoDceYtxJ0sI8G9mvp3WQMyTb1gKPTnj3hLcuC9GGfPXZ1WqxWK3zvdvIK_MqjYqh2MXi9t8pQo1ti9mvp3WQMyTb1gKPTnj3hLeCevmTYs-vBxfa3ksb_gQ4rRccFa3cJIt1v4yqRLssW9mvp3WQMyTb1gKPTnj3hLgai5ZbuE04drlCmpVIHMlVMYz6oUEtgIyKM7_TP_8OS9mvp3WQMyTb1gKPTnj3hLgts7zrT2CEPOnZfD0YfNm1lBzT3oEA5YbyvaVjdXX2e9mvp3WQMyTb1gKPTnj3hLiVqXhfYXyh1-1E_BoUcLcfPxIjhi2f-dzDri35IWPa-9mvp3WQMyTb1gKPTnj3hLitZIWfGVtfWNr6qUC2phZ6zWeohuj0aTZBdyIcVZRbm9mvp3WQMyTb1gKPTnj3hLi_Q0tJ4AzPcVAqLNkAhlywHsOz2gPDW-UF_fe9Vj9SG9mvp3WQMyTb1gKPTnj3hLjY6iic_nChwHq3NlyyjuUe3TPPJQbeiI-63WDr-ASBy9mvp3WQMyTb1gKPTnj3hLmZjTY8SRvha9dLoQuU2SkQAWEXNv3KZDo2ZkvA8xWkK9mvp3WQMyTb1gKPTnj3hLnkppFzFhaC5V_ej-9WDriOwf_tyvBAAfZsDK9weytzS9mvp3WQMyTb1gKPTnj3hLprUVHzZV06KvDUcnWXlr5KQgDlSVp9hpdUxISIV0CKe9mvp3WQMyTb1gKPTnj3hLp_MvUI1OsP6tmgh--U7RugrsXeb372_wpiuTvt9dRY29mvp3WQMyTb1gKPTnj3hLrWgm4ZRtJtPq82hciNl9hd47Tcs9DuKugccFYNE8VyC9mvp3WQMyTb1gKPTnj3hLruuuMVEnEnPtlaouZxE57TGphWcwOjMimPg3CanVWqG9mvp3WQMyTb1gKPTnj3hLr-IDCvt9LNoT-fozOgLjT2dRr-wRsBDbzUQf30xAArO9mvp3WQMyTb1gKPTnj3hLtU8e5jZjH61oBY07CTcDGsG5DMsXBio5HMpor9vkDVW9mvp3WQMyTb1gKPTnj3hLuPB42YOiSsQzIWOTiDUUzZMsM68Y591wiEyCNfQnVza9mvp3WQMyTb1gKPTnj3hLuXoIiQWcA1_x1UtttTsRuXZmaNn5VSR8AosoN9egTNm9mvp3WQMyTb1gKPTnj3hLvIemaOgZZkictQjugFGDwZ5qzSTPF3mcoGLS44TaDqe9mvp3WQMyTb1gKPTnj3hLxAm9rEd1rdjbkqoitbcY-4yUoUYsH-mkFrldijOIwvy9mvp3WQMyTb1gKPTnj3hLxhfBqLHuKoEcKLWoG0yD18mLWwwnKB1hAgfr5pLCln-9mvp3WQMyTb1gKPTnj3hLyQ8zZhe45_mDl1QTyZfxC3PrJn_YoQy5472_xmer24u9mvp3WQMyTb1gKPTnj3hLy2uFi0DToJh2WBW1ksFRSklgT6_WCBnaiNenbpd4_ve9mvp3WQMyTb1gKPTnj3hLzjv6vlnWfOisjf1KFvfEPvnCJFE_vGVZV9c1-in1QM69mvp3WQMyTb1gKPTnj3hL2MvrlzX1Zys2fk-WzcdJaWFdFwdK6WxyTULOAhb48_q9mvp3WQMyTb1gKPTnj3hL6Swi6ztWQtZcHsLK8kfSWUc47rt_s4QaRJVvWeKw0fq9mvp3WQMyTb1gKPTnj3hL_d1uPSnqhMd0Pa1KG9vHALX-wbOR-TJAkIasxx3W29i9mvp3WQMyTb1gKPTnj3hL_mOoT3gsydP88sz5_BH70Ddgh4dY0mKF7RJmm9xubQG9mvp3WQMyTb1gKPTnj3hL_s-yMtEumUttSF0scWdyiqVSWYStXKYedRYiHweaFDa9mvp3WQMyTb1gKPTnj3hLyhcdYUqNfhIck5SM0P5V05mB-dEaqW4DRQpBUifCxlu9mvp3WQMyTb1gKPTnj3hLVbmbDeU9vP5IWqnHN88_thbvPZH6tZmqfKsZ7adjtbq9mvp3WQMyTb1gKPTnj3hLd90ZD6MNiP9eOwEaCuYeYgl4DBMLU17Lh-bwiIoLay-9mvp3WQMyTb1gKPTnj3hLyDyxOSKtmfVgdEZ13TfMlNytWh_Lpkcv7jQRcdk56IS9mvp3WQMyTb1gKPTnj3hLOwKHUz4Mw9DsGqgjy_CpQarYchV50cSZgC3Rw6Y2uKm9mvp3WQMyTb1gKPTnj3hLk5ru9PX6UeIzQMPy5JBIzohyUmr991LDp_Oj8ryfYEm9mvp3WQMyTb1gKPTnj3hLZFdb2RJ4mi4UrVb2NB9Sr2v4DPlEAHhZdenwTi1k10W9mvp3WQMyTb1gKPTnj3hLRcfIrnUKz7tI_DdSfWQS3WRNrtiRPM2KJMlNhWln344=", + "fileType": "BIN" + } + ] + }, + "architecture": "X86_64" + } + ], + "metadata": { + "kind": "compute#metadata", + "fingerprint": "jBFO10OSSew=" + }, + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie", + "scheduling": { + "onHostMaintenance": "MIGRATE", + "automaticRestart": true, + "preemptible": false, + "provisioningModel": "STANDARD" + }, + "cpuPlatform": "AMD Rome", + "labels": { + "goog-terraform-provisioned": "true" + }, + "labelFingerprint": "vezUS-42LLM=", + "startRestricted": false, + "deletionProtection": false, + "shieldedInstanceConfig": { + "enableSecureBoot": false, + "enableVtpm": true, + "enableIntegrityMonitoring": true + }, + "shieldedInstanceIntegrityPolicy": { + "updateAutoLearnPolicy": true + }, + "fingerprint": "3mCc7YfgPgg=", + "lastStartTimestamp": "2026-03-13T08:36:30.685-07:00", + "satisfiesPzi": true, + "resourceStatus": { + "effectiveInstanceMetadata": { + "vmDnsSettingMetadataValue": "ZonalOnly", + "enableOsloginMetadataValue": true + } + } + } + ] + }, + "zones/us-central1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-central1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-central1-b" + } + ] + } + }, + "zones/us-central1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-central1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-central1-c" + } + ] + } + }, + "zones/us-central1-f": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-central1-f'.", + "data": [ + { + "key": "scope", + "value": "zones/us-central1-f" + } + ] + } + }, + "zones/europe-west1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west1-b" + } + ] + } + }, + "zones/europe-west1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west1-c" + } + ] + } + }, + "zones/europe-west1-d": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west1-d'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west1-d" + } + ] + } + }, + "zones/us-west1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west1-a" + } + ] + } + }, + "zones/us-west1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west1-b" + } + ] + } + }, + "zones/us-west1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west1-c" + } + ] + } + }, + "zones/asia-east1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east1-a" + } + ] + } + }, + "zones/asia-east1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east1-b" + } + ] + } + }, + "zones/asia-east1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east1-c" + } + ] + } + }, + "zones/us-east1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east1-b" + } + ] + } + }, + "zones/us-east1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east1-c" + } + ] + } + }, + "zones/us-east1-d": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east1-d'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east1-d" + } + ] + } + }, + "zones/asia-northeast1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast1-a" + } + ] + } + }, + "zones/asia-northeast1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast1-b" + } + ] + } + }, + "zones/asia-northeast1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast1-c" + } + ] + } + }, + "zones/asia-southeast1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast1-a" + } + ] + } + }, + "zones/asia-southeast1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast1-b" + } + ] + } + }, + "zones/asia-southeast1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast1-c" + } + ] + } + }, + "zones/us-east4-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east4-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east4-a" + } + ] + } + }, + "zones/us-east4-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east4-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east4-b" + } + ] + } + }, + "zones/us-east4-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east4-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east4-c" + } + ] + } + }, + "zones/australia-southeast1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast1-c" + } + ] + } + }, + "zones/australia-southeast1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast1-a" + } + ] + } + }, + "zones/australia-southeast1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast1-b" + } + ] + } + }, + "zones/europe-west2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west2-a" + } + ] + } + }, + "zones/europe-west2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west2-b" + } + ] + } + }, + "zones/europe-west2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west2-c" + } + ] + } + }, + "zones/europe-west3-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west3-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west3-c" + } + ] + } + }, + "zones/europe-west3-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west3-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west3-a" + } + ] + } + }, + "zones/europe-west3-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west3-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west3-b" + } + ] + } + }, + "zones/southamerica-east1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-east1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-east1-a" + } + ] + } + }, + "zones/southamerica-east1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-east1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-east1-b" + } + ] + } + }, + "zones/southamerica-east1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-east1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-east1-c" + } + ] + } + }, + "zones/asia-south1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south1-b" + } + ] + } + }, + "zones/asia-south1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south1-a" + } + ] + } + }, + "zones/asia-south1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south1-c" + } + ] + } + }, + "zones/northamerica-northeast1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast1-a" + } + ] + } + }, + "zones/northamerica-northeast1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast1-b" + } + ] + } + }, + "zones/northamerica-northeast1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast1-c" + } + ] + } + }, + "zones/europe-west4-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west4-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west4-c" + } + ] + } + }, + "zones/europe-west4-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west4-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west4-b" + } + ] + } + }, + "zones/europe-west4-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west4-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west4-a" + } + ] + } + }, + "zones/europe-north1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north1-b" + } + ] + } + }, + "zones/europe-north1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north1-c" + } + ] + } + }, + "zones/europe-north1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north1-a" + } + ] + } + }, + "zones/us-west2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west2-c" + } + ] + } + }, + "zones/us-west2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west2-b" + } + ] + } + }, + "zones/us-west2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west2-a" + } + ] + } + }, + "zones/asia-east2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east2-c" + } + ] + } + }, + "zones/asia-east2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east2-b" + } + ] + } + }, + "zones/asia-east2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-east2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-east2-a" + } + ] + } + }, + "zones/europe-west6-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west6-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west6-b" + } + ] + } + }, + "zones/europe-west6-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west6-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west6-c" + } + ] + } + }, + "zones/europe-west6-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west6-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west6-a" + } + ] + } + }, + "zones/asia-northeast2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast2-b" + } + ] + } + }, + "zones/asia-northeast2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast2-c" + } + ] + } + }, + "zones/asia-northeast2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast2-a" + } + ] + } + }, + "zones/asia-northeast3-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast3-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast3-a" + } + ] + } + }, + "zones/asia-northeast3-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast3-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast3-c" + } + ] + } + }, + "zones/asia-northeast3-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-northeast3-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-northeast3-b" + } + ] + } + }, + "zones/us-west3-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west3-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west3-a" + } + ] + } + }, + "zones/us-west3-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west3-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west3-b" + } + ] + } + }, + "zones/us-west3-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west3-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west3-c" + } + ] + } + }, + "zones/us-west4-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west4-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west4-c" + } + ] + } + }, + "zones/us-west4-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west4-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west4-a" + } + ] + } + }, + "zones/us-west4-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-west4-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-west4-b" + } + ] + } + }, + "zones/asia-southeast2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast2-a" + } + ] + } + }, + "zones/asia-southeast2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast2-c" + } + ] + } + }, + "zones/asia-southeast2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast2-b" + } + ] + } + }, + "zones/europe-central2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-central2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-central2-b" + } + ] + } + }, + "zones/europe-central2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-central2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-central2-c" + } + ] + } + }, + "zones/europe-central2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-central2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-central2-a" + } + ] + } + }, + "zones/northamerica-northeast2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast2-b" + } + ] + } + }, + "zones/northamerica-northeast2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast2-a" + } + ] + } + }, + "zones/northamerica-northeast2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-northeast2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-northeast2-c" + } + ] + } + }, + "zones/asia-south2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south2-a" + } + ] + } + }, + "zones/asia-south2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south2-c" + } + ] + } + }, + "zones/asia-south2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-south2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-south2-b" + } + ] + } + }, + "zones/australia-southeast2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast2-a" + } + ] + } + }, + "zones/australia-southeast2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast2-c" + } + ] + } + }, + "zones/australia-southeast2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/australia-southeast2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/australia-southeast2-b" + } + ] + } + }, + "zones/southamerica-west1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-west1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-west1-a" + } + ] + } + }, + "zones/southamerica-west1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-west1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-west1-b" + } + ] + } + }, + "zones/southamerica-west1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/southamerica-west1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/southamerica-west1-c" + } + ] + } + }, + "zones/europe-west8-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west8-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west8-a" + } + ] + } + }, + "zones/europe-west8-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west8-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west8-b" + } + ] + } + }, + "zones/europe-west8-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west8-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west8-c" + } + ] + } + }, + "zones/europe-west9-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west9-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west9-b" + } + ] + } + }, + "zones/europe-west9-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west9-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west9-a" + } + ] + } + }, + "zones/europe-west9-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west9-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west9-c" + } + ] + } + }, + "zones/us-east5-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east5-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east5-c" + } + ] + } + }, + "zones/us-east5-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east5-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east5-b" + } + ] + } + }, + "zones/us-east5-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-east5-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-east5-a" + } + ] + } + }, + "zones/europe-southwest1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-southwest1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-southwest1-b" + } + ] + } + }, + "zones/europe-southwest1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-southwest1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-southwest1-a" + } + ] + } + }, + "zones/europe-southwest1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-southwest1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-southwest1-c" + } + ] + } + }, + "zones/us-south1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-south1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/us-south1-c" + } + ] + } + }, + "zones/us-south1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-south1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/us-south1-a" + } + ] + } + }, + "zones/us-south1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/us-south1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/us-south1-b" + } + ] + } + }, + "zones/me-west1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-west1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/me-west1-b" + } + ] + } + }, + "zones/me-west1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-west1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/me-west1-a" + } + ] + } + }, + "zones/me-west1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-west1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/me-west1-c" + } + ] + } + }, + "zones/europe-west12-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west12-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west12-c" + } + ] + } + }, + "zones/europe-west12-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west12-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west12-a" + } + ] + } + }, + "zones/europe-west12-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west12-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west12-b" + } + ] + } + }, + "zones/me-central1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central1-a" + } + ] + } + }, + "zones/me-central1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central1-b" + } + ] + } + }, + "zones/me-central1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central1-c" + } + ] + } + }, + "zones/europe-west10-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west10-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west10-c" + } + ] + } + }, + "zones/europe-west10-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west10-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west10-a" + } + ] + } + }, + "zones/europe-west10-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-west10-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-west10-b" + } + ] + } + }, + "zones/me-central2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central2-c" + } + ] + } + }, + "zones/me-central2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central2-a" + } + ] + } + }, + "zones/me-central2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/me-central2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/me-central2-b" + } + ] + } + }, + "zones/africa-south1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/africa-south1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/africa-south1-b" + } + ] + } + }, + "zones/africa-south1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/africa-south1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/africa-south1-a" + } + ] + } + }, + "zones/africa-south1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/africa-south1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/africa-south1-c" + } + ] + } + }, + "zones/northamerica-south1-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-south1-b'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-south1-b" + } + ] + } + }, + "zones/northamerica-south1-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-south1-a'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-south1-a" + } + ] + } + }, + "zones/northamerica-south1-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/northamerica-south1-c'.", + "data": [ + { + "key": "scope", + "value": "zones/northamerica-south1-c" + } + ] + } + }, + "zones/europe-north2-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north2-c'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north2-c" + } + ] + } + }, + "zones/europe-north2-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north2-a'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north2-a" + } + ] + } + }, + "zones/europe-north2-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/europe-north2-b'.", + "data": [ + { + "key": "scope", + "value": "zones/europe-north2-b" + } + ] + } + }, + "zones/asia-southeast3-b": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast3-b'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast3-b" + } + ] + } + }, + "zones/asia-southeast3-c": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast3-c'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast3-c" + } + ] + } + }, + "zones/asia-southeast3-a": { + "warning": { + "code": "NO_RESULTS_ON_PAGE", + "message": "No results for the scope 'zones/asia-southeast3-a'.", + "data": [ + { + "key": "scope", + "value": "zones/asia-southeast3-a" + } + ] + } + } + }, + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/instances" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json new file mode 100644 index 00000000000..55acf2cedd2 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/get-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json @@ -0,0 +1,140 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 13 Mar 2026 15:36:45 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "12944", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie?alt=json" + }, + "body": { + "kind": "compute#instance", + "id": "7368592281459855388", + "creationTimestamp": "2026-03-13T08:36:19.675-07:00", + "name": "sacred-dassie", + "tags": { + "fingerprint": "42WmSpB8rSM=" + }, + "machineType": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/machineTypes/e2-micro", + "status": "RUNNING", + "zone": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a", + "canIpForward": true, + "networkInterfaces": [ + { + "kind": "compute#networkInterface", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "subnetwork": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/sacred-dassie-subnet", + "networkIP": "10.2.0.2", + "name": "nic0", + "accessConfigs": [ + { + "kind": "compute#accessConfig", + "type": "ONE_TO_ONE_NAT", + "name": "external-nat", + "natIP": "35.225.226.220", + "networkTier": "PREMIUM" + } + ], + "fingerprint": "s-mVbaV9s2M=", + "stackType": "IPV4_ONLY" + } + ], + "disks": [ + { + "kind": "compute#attachedDisk", + "type": "PERSISTENT", + "mode": "READ_WRITE", + "source": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/disks/sacred-dassie", + "deviceName": "persistent-disk-0", + "index": 0, + "boot": true, + "autoDelete": true, + "licenses": [ + "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/licenses/ubuntu-2204-lts" + ], + "interface": "SCSI", + "guestOsFeatures": [ + { + "type": "VIRTIO_SCSI_MULTIQUEUE" + }, + { + "type": "SEV_CAPABLE" + }, + { + "type": "SEV_SNP_CAPABLE" + }, + { + "type": "SEV_LIVE_MIGRATABLE" + }, + { + "type": "SEV_LIVE_MIGRATABLE_V2" + }, + { + "type": "IDPF" + }, + { + "type": "TDX_CAPABLE" + }, + { + "type": "UEFI_COMPATIBLE" + }, + { + "type": "GVNIC" + } + ], + "diskSizeGb": "10", + "shieldedInstanceInitialState": { + "dbxs": [ + { + "content": "2gcDBhMRFQAAAAAAAAAAABENAAAAAvEOndKvSt9o7kmKqTR9N1ZlpzCCDPUCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGgggsIMIIFGDCCBACgAwIBAgITMwAAABNryScg3e1ZiAAAAAAAEzANBgkqhkiG9w0BAQsFADCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEqMCgGA1UEAxMhTWljcm9zb2Z0IENvcnBvcmF0aW9uIEtFSyBDQSAyMDExMB4XDTE2MDEwNjE4MzQxNVoXDTE3MDQwNjE4MzQxNVowgZUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xDTALBgNVBAsTBE1PUFIxMDAuBgNVBAMTJ01pY3Jvc29mdCBXaW5kb3dzIFVFRkkgS2V5IEV4Y2hhbmdlIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXiCkZgbboTnVZnS1h_JbnlcVst9wtFK8NQjTpeB9wirml3h-fzi8vzki0hSNBD2Dg49lGEvs4egyowmTsLu1TnBUH1f_Hi8Noa7fKXV6F93qYrTPajx5v9L7NedplWnMEPsRvJrQdrysTZwtoXMLYDhc8bQHI5nlJDfgqrB8JiC4A3vL9i19lkQOTq4PZb5AcVcE0wlG7lR_btoQN0g5B4_7pI2S_9mU1PXr1NBSEl48Kl4cJwO2GyvOVvxQ6wUSFTExmCBKrT3LnPU5lZY68n3MpZ5VY4skhrEt2dyf5bZNzkYTTouxC0n37OrMbGGq3tpv7JDD6E_Rfqua3dXYECAwEAAaOCAXIwggFuMBQGA1UdJQQNMAsGCSsGAQQBgjdPATAdBgNVHQ4EFgQUVsJIppTfox2XYoAJRIlnxAUOy2owUQYDVR0RBEowSKRGMEQxDTALBgNVBAsTBE1PUFIxMzAxBgNVBAUTKjMxNjMxKzJjNDU2Y2JjLTA1NDItNDdkOS05OWU1LWQzOWI4MTVjNTczZTAfBgNVHSMEGDAWgBRi_EPNoD6ky2cS0lvZVax7zLaKXzBTBgNVHR8ETDBKMEigRqBEhkJodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb3JLRUtDQTIwMTFfMjAxMS0wNi0yNC5jcmwwYAYIKwYBBQUHAQEEVDBSMFAGCCsGAQUFBzAChkRodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvcktFS0NBMjAxMV8yMDExLTA2LTI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCGjTFLjxsKmyLESJueg0S2Cp8N7MOq2IALsitZHwfYw2jMhY9b9kmKvIdSqVna1moZ6_zJSOS_JY6HkWZr6dDJe9Lj7xiW_e4qPP-KDrCVb02vBnK4EktVjTdJpyMhxBMdXUcq1eGl6518oCkQ27tu0-WZjaWEVsEY_gpQj0ye2UA4HYUYgJlpT24oJRi7TeQ03Nebb-ZrUkbf9uxl0OVV_mg2R5FDwOc3REoRAgv5jnw6X7ha5hlRCl2cLF27TFrFIRQQT4eSM33eDiitXXpYmD13jqKeHhLVXr07QSwqvKe1o1UYokJngP0pTwoDnt2qRuLnZ71jw732dSPN9B57MIIF6DCCA9CgAwIBAgIKYQrRiAAAAAAAAzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE7MDkGA1UEAxMyTWljcm9zb2Z0IENvcnBvcmF0aW9uIFRoaXJkIFBhcnR5IE1hcmtldHBsYWNlIFJvb3QwHhcNMTEwNjI0MjA0MTI5WhcNMjYwNjI0MjA1MTI5WjCBgDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEqMCgGA1UEAxMhTWljcm9zb2Z0IENvcnBvcmF0aW9uIEtFSyBDQSAyMDExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxOi1ir-tVyawJsPq5_tXekQCXQcN2krldCrmsA_sbevsf7njWmMyfBEXTw7jC6c4FZOOxvXghLGamyzn9beR1gnh4sAEqKwwHN9I8wZQmmSnUX_IhU-PIIbO_i_hn_-CwO3pzc70U2piOgtDueIl_f4F-dTEFKsR4iOJjXC3pB1N7K7lnPoWwtfBy9ToxC_lme4kiwPsjfKL6sNK-0MREgt-tUeSbNzmBInr9TME6xABKnHl-YMTPP8lCS9odkb_uk--3K1xKliq-w7SeT3km2U7zCkqn_xyWaLrrpLv9jUTgMYC7ORfzJ12ze9jksGveUCEeYd_41Ko6J17B2mPFQIDAQABo4IBTzCCAUswEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGL8Q82gPqTLZxLSW9lVrHvMtopfMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH_MB8GA1UdIwQYMBaAFEVmUkPhflgRv9ZOniNVCDs6ImqoMFwGA1UdHwRVMFMwUaBPoE2GS2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY0NvclRoaVBhck1hclJvb18yMDEwLTEwLTA1LmNybDBgBggrBgEFBQcBAQRUMFIwUAYIKwYBBQUHMAKGRGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljQ29yVGhpUGFyTWFyUm9vXzIwMTAtMTAtMDUuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQDUhIj1FJQYAsoqPPsqkhwM16DR8ehSZqjuorV1epAAqi2kdlrqebe5N2pRexBk9uFk8gJnvveoG3i9us6IWGQM1lfIGaNfBdbbxtBpzkhLMrfrXdIw9cD1uLp4B6Mr_pvbNFaE7ILKrkElcJxr6f6QD9eWH-XnlB-yKgyNS_8oKRB799d8pdF2uQXIee0PkJKcwv7fb35sD3vUwUXdNFGWOQ_lXlbYGAWW9AemQrOgd_0IGfJxVsyfhiOkh8um_Vh-1GlnFZF-gfJ_E-UNi4o8h4Tr4869Q-WtLYSTjmorWnxE-lKqgcgtHLvgUt8AEfiaPcFgsOEztaOI0WUZChrnrHykwYKHTjixLw3FFIdv_Y0uvDm25-bD4OTNJ4TvlELvKYuQRkE7gRtn2PlDWWXLDbz9AJJP9HU7p6kk_FBBQHngLU8Kaid2blLtlml7rw_3hwXQRcKtUxSBH_swBKo3NmHaSmkbNNho7dYCz2yUDNPPbCJ5rbHwvAOiRmCpxAfCIYLx_fLoeTJgv9ispSIUS8rB2EvrfT9XNbLmT3W0sGADIlOukXkd1ptBHxWGVHCy3g01D3ywNHK6l2A78HnrorIcXaIWuIfF6Rv2tZclbzif45H6inmYw2kOt6McIAWX-MoUrgDXxPPAFBB1azSgG7WZYPNcsMVXTjbSMoS_njGCAcQwggHAAgEBMIGYMIGAMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSowKAYDVQQDEyFNaWNyb3NvZnQgQ29ycG9yYXRpb24gS0VLIENBIDIwMTECEzMAAAATa8knIN3tWYgAAAAAABMwDQYJYIZIAWUDBAIBBQAwDQYJKoZIhvcNAQEBBQAEggEAhabaxRIJ7nUZ-m__mIG0lII6yD-lxoeI8S83ZKTP8Qx5h5asySWl7420eGhna7zyaVRvVVIhkjOMIfcKr29LgzQpYDqPUc8aYAdGCsZKZGmHCMjEulnq5TDK79GKinzZfb2sAWXEJ68N8oNnY7faBKjHjmmJbAEz8ufE4DijgJ_NBov2xmhTZyNHQ7pB1iCdrEUGObzdJc0Qtmh3CNOEcmH0ukd8sTHE9acBBTFHS8dvreR_sP7dXClZJbJiWAFKvQn3EjCTiYizkZ4I_5xiqjHELht_ORQKN-Hnoqnl4kcRINhZRV7JlgAQDlBJLv3OTjShRO_ZWCdcu7PtwhweiSYWxMFMUJJArKlB-TaTQyiMDgAAAAAAADAAAAC9mvp3WQMyTb1gKPTnj3hLgLTZaTG_DQL9kaYeGdFPHaRS5m2yQIyoYE1BH5Jlnwq9mvp3WQMyTb1gKPTnj3hL9S-Do_qc-9aSD3IoJNvkA0U00luFByRrO5V9rG4bznq9mvp3WQMyTb1gKPTnj3hLxdnYoYbiyC0Jr6oqb38uc4cNPmT3LE4I72d5aoQPD729mvp3WQMyTb1gKPTnj3hLNjOE0U0fLgt4FWJkhMRZrVejGO9DliZgSNBYxaGbv3a9mvp3WQMyTb1gKPTnj3hLGuyEuEtsZaUSIKm-cYGWUjAhDWLW0zxImZxrKVorCga9mvp3WQMyTb1gKPTnj3hL5spo6UFGYprwP2nC-G5r72L5MLN8b7zIeLeN-YwDNOW9mvp3WQMyTb1gKPTnj3hLw6maRg2kZKBXw1htg8719K4ItxA5ee2JMnQt8O1TDGa9mvp3WQMyTb1gKPTnj3hLWPuUGu-VollDs_tfJRCg3z_kTFjJXgq4BIcpdWirl3G9mvp3WQMyTb1gKPTnj3hLU5HDovsRIQKmqh7cJa534Z9dbwnNCe6yUJkiv81Zkuq9mvp3WQMyTb1gKPTnj3hL1iYVfh1qcYvBJKuNony7ZQcsoDp7ayV9vcu9YPZe89G9mvp3WQMyTb1gKPTnj3hL0GPsKPZ-ulPxZC2_ff8zxqMq3YafYBP-Fi4sMvHL5W29mvp3WQMyTb1gKPTnj3hLKcbrUrQ8OqGLLNjtbqhgfO88-uG6_hFldVzy5hSESkS9mvp3WQMyTb1gKPTnj3hLkPvnDmnWM0CNPhcMaDLbstIJ4CclJ9-2PUnSlXKm9Ey9mvp3WQMyTb1gKPTnj3hLB17qBgWJVIugYLL-7RDaPCDH_psXzQJrlOimg7gRUji9mvp3WQMyTb1gKPTnj3hLB-bGqFhkb7HvxnkD_iixFgEfI2f-kua-KzaZnv850J69mvp3WQMyTb1gKPTnj3hLCd9fTlESCOx4uW0S0IEl_bYDho3jn29yknhSWZtlnCa9mvp3WQMyTb1gKPTnj3hLC7tDktqseribMKSsZXUxuXv6qwT5Cw2v5fm265CgY3S9mvp3WQMyTb1gKPTnj3hLDBiTOXYt8zarPdAGpGPfcVo5z7D0kkZcYA5sa9e9iYy9mvp3WQMyTb1gKPTnj3hLDQ2-ym8p7KBvMxp9cuSISxIJf7NImDoqFKDXP08QFA-9mvp3WQMyTb1gKPTnj3hLDcnz-5mWIUjDyoM2MnWNPtT8jQsAB7lbMeZSjyrNW_y9mvp3WQMyTb1gKPTnj3hLEG-s6s_s_U4wO3T0gKCAmOLQgCuTb47HdM4h8xaGaJy9mvp3WQMyTb1gKPTnj3hLF046C1tDxqYHu9NATwU0Hj3POWJnzpT4tQ4uI6nakgy9mvp3WQMyTb1gKPTnj3hLGDM0Kf8FYu2flwM-EUjc7uUtvi5JbVQQtc_WyGTS0Q-9mvp3WQMyTb1gKPTnj3hLK5nPJkIukv42X79Lww0nCGye4Ut6b_9E-y9rkAFpmTm9mvp3WQMyTb1gKPTnj3hLK78sp7jx2R8n7lK2-ypd0Em4WiubUpxdZmIGgQSwVfi9mvp3WQMyTb1gKPTnj3hLLHPZMyW6bcvlidSkxjxbk1VZ75L78FDtUMTiCFIG8X29mvp3WQMyTb1gKPTnj3hLLnCRZ4am93NRH6cYH6sPHXC1V8YyLqkjsqjTuStRr329mvp3WQMyTb1gKPTnj3hLMGYo-lR3MFcoukpGfefQOHpU9WnTdp_OXnXsidKNFZO9mvp3WQMyTb1gKPTnj3hLNgjtuvWtD0GkFKF3er8vr15nAzRnXsOZXmk1gp4MqtK9mvp3WQMyTb1gKPTnj3hLOEHSITaNFYPXXAoC5iFgOU1sTgpnYLb2B7kDYryFWwK9mvp3WQMyTb1gKPTnj3hLP86bn98-8J1UUrD5XuSBwrfwbXQ6c3lxVY5wE2rOPnO9mvp3WQMyTb1gKPTnj3hLQ5fayoOef2MHfLUMkt9DvC0vsqj1nyb8eg5L1Nl1FpK9mvp3WQMyTb1gKPTnj3hLR8wIYSfiBpqG4Dpr7yzUEPjFWm1r2zYhaMMbLOMqWt-9mvp3WQMyTb1gKPTnj3hLUYgx_nOCtRTQPhXGISKLirZUeb0Mv6PFwdD0jZwwYTW9mvp3WQMyTb1gKPTnj3hLWulJ6ohV65PkOdvGW9ouQoUsL99nifoUZzbjw0EPK1y9mvp3WQMyTb1gKPTnj3hLax0TgHjkQYqmjet7s14GYJLPR57rjOTNEufQcsy0L2a9mvp3WQMyTb1gKPTnj3hLbIhUR43VWeKTUbgmwGy4v-8rlK01ODWHctGT-C7RyhG9mvp3WQMyTb1gKPTnj3hLbxQo_3HJ2w7Vrx8ue7_Lq2R8wmXd9bKTzbYm9Qo6eF69mvp3WQMyTb1gKPTnj3hLcfKQb9IiSX5Uo0ZiqySX_MgQIHcP9RNo6ePZv8v9Y3W9mvp3WQMyTb1gKPTnj3hLcms-tlQEajDz-D2bls4D9nDpqAbRcIoDceYtxJ0sI8G9mvp3WQMyTb1gKPTnj3hLcuC9GGfPXZ1WqxWK3zvdvIK_MqjYqh2MXi9t8pQo1ti9mvp3WQMyTb1gKPTnj3hLeCevmTYs-vBxfa3ksb_gQ4rRccFa3cJIt1v4yqRLssW9mvp3WQMyTb1gKPTnj3hLgai5ZbuE04drlCmpVIHMlVMYz6oUEtgIyKM7_TP_8OS9mvp3WQMyTb1gKPTnj3hLgts7zrT2CEPOnZfD0YfNm1lBzT3oEA5YbyvaVjdXX2e9mvp3WQMyTb1gKPTnj3hLiVqXhfYXyh1-1E_BoUcLcfPxIjhi2f-dzDri35IWPa-9mvp3WQMyTb1gKPTnj3hLitZIWfGVtfWNr6qUC2phZ6zWeohuj0aTZBdyIcVZRbm9mvp3WQMyTb1gKPTnj3hLi_Q0tJ4AzPcVAqLNkAhlywHsOz2gPDW-UF_fe9Vj9SG9mvp3WQMyTb1gKPTnj3hLjY6iic_nChwHq3NlyyjuUe3TPPJQbeiI-63WDr-ASBy9mvp3WQMyTb1gKPTnj3hLmZjTY8SRvha9dLoQuU2SkQAWEXNv3KZDo2ZkvA8xWkK9mvp3WQMyTb1gKPTnj3hLnkppFzFhaC5V_ej-9WDriOwf_tyvBAAfZsDK9weytzS9mvp3WQMyTb1gKPTnj3hLprUVHzZV06KvDUcnWXlr5KQgDlSVp9hpdUxISIV0CKe9mvp3WQMyTb1gKPTnj3hLp_MvUI1OsP6tmgh--U7RugrsXeb372_wpiuTvt9dRY29mvp3WQMyTb1gKPTnj3hLrWgm4ZRtJtPq82hciNl9hd47Tcs9DuKugccFYNE8VyC9mvp3WQMyTb1gKPTnj3hLruuuMVEnEnPtlaouZxE57TGphWcwOjMimPg3CanVWqG9mvp3WQMyTb1gKPTnj3hLr-IDCvt9LNoT-fozOgLjT2dRr-wRsBDbzUQf30xAArO9mvp3WQMyTb1gKPTnj3hLtU8e5jZjH61oBY07CTcDGsG5DMsXBio5HMpor9vkDVW9mvp3WQMyTb1gKPTnj3hLuPB42YOiSsQzIWOTiDUUzZMsM68Y591wiEyCNfQnVza9mvp3WQMyTb1gKPTnj3hLuXoIiQWcA1_x1UtttTsRuXZmaNn5VSR8AosoN9egTNm9mvp3WQMyTb1gKPTnj3hLvIemaOgZZkictQjugFGDwZ5qzSTPF3mcoGLS44TaDqe9mvp3WQMyTb1gKPTnj3hLxAm9rEd1rdjbkqoitbcY-4yUoUYsH-mkFrldijOIwvy9mvp3WQMyTb1gKPTnj3hLxhfBqLHuKoEcKLWoG0yD18mLWwwnKB1hAgfr5pLCln-9mvp3WQMyTb1gKPTnj3hLyQ8zZhe45_mDl1QTyZfxC3PrJn_YoQy5472_xmer24u9mvp3WQMyTb1gKPTnj3hLy2uFi0DToJh2WBW1ksFRSklgT6_WCBnaiNenbpd4_ve9mvp3WQMyTb1gKPTnj3hLzjv6vlnWfOisjf1KFvfEPvnCJFE_vGVZV9c1-in1QM69mvp3WQMyTb1gKPTnj3hL2MvrlzX1Zys2fk-WzcdJaWFdFwdK6WxyTULOAhb48_q9mvp3WQMyTb1gKPTnj3hL6Swi6ztWQtZcHsLK8kfSWUc47rt_s4QaRJVvWeKw0fq9mvp3WQMyTb1gKPTnj3hL_d1uPSnqhMd0Pa1KG9vHALX-wbOR-TJAkIasxx3W29i9mvp3WQMyTb1gKPTnj3hL_mOoT3gsydP88sz5_BH70Ddgh4dY0mKF7RJmm9xubQG9mvp3WQMyTb1gKPTnj3hL_s-yMtEumUttSF0scWdyiqVSWYStXKYedRYiHweaFDa9mvp3WQMyTb1gKPTnj3hLyhcdYUqNfhIck5SM0P5V05mB-dEaqW4DRQpBUifCxlu9mvp3WQMyTb1gKPTnj3hLVbmbDeU9vP5IWqnHN88_thbvPZH6tZmqfKsZ7adjtbq9mvp3WQMyTb1gKPTnj3hLd90ZD6MNiP9eOwEaCuYeYgl4DBMLU17Lh-bwiIoLay-9mvp3WQMyTb1gKPTnj3hLyDyxOSKtmfVgdEZ13TfMlNytWh_Lpkcv7jQRcdk56IS9mvp3WQMyTb1gKPTnj3hLOwKHUz4Mw9DsGqgjy_CpQarYchV50cSZgC3Rw6Y2uKm9mvp3WQMyTb1gKPTnj3hLk5ru9PX6UeIzQMPy5JBIzohyUmr991LDp_Oj8ryfYEm9mvp3WQMyTb1gKPTnj3hLZFdb2RJ4mi4UrVb2NB9Sr2v4DPlEAHhZdenwTi1k10W9mvp3WQMyTb1gKPTnj3hLRcfIrnUKz7tI_DdSfWQS3WRNrtiRPM2KJMlNhWln344=", + "fileType": "BIN" + } + ] + }, + "architecture": "X86_64" + } + ], + "metadata": { + "kind": "compute#metadata", + "fingerprint": "jBFO10OSSew=" + }, + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie", + "scheduling": { + "onHostMaintenance": "MIGRATE", + "automaticRestart": true, + "preemptible": false, + "provisioningModel": "STANDARD" + }, + "cpuPlatform": "AMD Rome", + "labels": { + "goog-terraform-provisioned": "true" + }, + "labelFingerprint": "vezUS-42LLM=", + "startRestricted": false, + "deletionProtection": false, + "shieldedInstanceConfig": { + "enableSecureBoot": false, + "enableVtpm": true, + "enableIntegrityMonitoring": true + }, + "shieldedInstanceIntegrityPolicy": { + "updateAutoLearnPolicy": true + }, + "fingerprint": "LTNbLIdOh_Q=", + "lastStartTimestamp": "2026-03-13T08:36:30.685-07:00", + "satisfiesPzi": true, + "resourceStatus": { + "effectiveInstanceMetadata": { + "vmDnsSettingMetadataValue": "ZonalOnly", + "enableOsloginMetadataValue": true + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/put-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/put-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json new file mode 100644 index 00000000000..cd3ed0ad7f4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/instance-set-can-ip-forward/put-compute-v1-projects-cloud-custodian-zones-us-central1-a-instances-sacred-dassie_1.json @@ -0,0 +1,31 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Fri, 13 Mar 2026 15:36:42 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "781", + "-content-encoding": "gzip" + }, + "body": { + "kind": "compute#operation", + "id": "5733706902858732005", + "name": "operation-1773416201811-64ce9a2bf7715-16ec3769-ace3a7e0", + "zone": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a", + "operationType": "update", + "targetLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie", + "targetId": "7368592281459855388", + "status": "RUNNING", + "user": "markos@sixfeetup.com", + "progress": 0, + "insertTime": "2026-03-13T08:36:42.598-07:00", + "startTime": "2026-03-13T08:36:42.623-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/operations/operation-1773416201811-64ce9a2bf7715-16ec3769-ace3a7e0" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/main.tf b/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/main.tf new file mode 100644 index 00000000000..c5aad713f87 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/main.tf @@ -0,0 +1,44 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id + billing_project = var.google_project_id + user_project_override = true +} + +resource "random_pet" "server" { +} + +resource "google_compute_network" "vpc" { + name = "${random_pet.server.id}-vpc" + auto_create_subnetworks = "false" + routing_mode = "GLOBAL" +} + +resource "google_compute_subnetwork" "network_subnet" { + name = "${random_pet.server.id}-subnet" + ip_cidr_range = "10.2.0.0/16" + network = google_compute_network.vpc.name + region = "us-central1" +} + +resource "google_compute_instance" "default" { + name = random_pet.server.id + machine_type = "e2-micro" + zone = "us-central1-a" + # can_ip_forward defaults to false — the test enables it via the action + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + } + } + + network_interface { + network = google_compute_network.vpc.name + subnetwork = google_compute_subnetwork.network_subnet.name + access_config {} + } +} diff --git a/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/tf_resources.json b/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/tf_resources.json new file mode 100644 index 00000000000..7c5806a2250 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_instance_set_can_ip_forward/tf_resources.json @@ -0,0 +1,219 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_compute_instance": { + "default": { + "advanced_machine_features": [], + "allow_stopping_for_update": null, + "attached_disk": [], + "boot_disk": [ + { + "auto_delete": true, + "device_name": "persistent-disk-0", + "disk_encryption_key_raw": "", + "disk_encryption_key_rsa": "", + "disk_encryption_key_sha256": "", + "disk_encryption_service_account": "", + "force_attach": false, + "guest_os_features": [ + "VIRTIO_SCSI_MULTIQUEUE", + "SEV_CAPABLE", + "SEV_SNP_CAPABLE", + "SEV_LIVE_MIGRATABLE", + "SEV_LIVE_MIGRATABLE_V2", + "IDPF", + "TDX_CAPABLE", + "UEFI_COMPATIBLE", + "GVNIC" + ], + "initialize_params": [ + { + "architecture": "X86_64", + "enable_confidential_compute": false, + "image": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/images/ubuntu-2204-jammy-v20260226", + "labels": {}, + "provisioned_iops": 0, + "provisioned_throughput": 0, + "resource_manager_tags": null, + "resource_policies": [], + "size": 10, + "snapshot": "", + "source_image_encryption_key": [], + "source_snapshot_encryption_key": [], + "storage_pool": "", + "type": "pd-standard" + } + ], + "interface": "", + "kms_key_self_link": "", + "mode": "READ_WRITE", + "source": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/disks/sacred-dassie" + } + ], + "can_ip_forward": false, + "confidential_instance_config": [], + "cpu_platform": "AMD Rome", + "creation_timestamp": "2026-03-13T08:36:19.675-07:00", + "current_status": "RUNNING", + "deletion_protection": false, + "description": "", + "desired_status": null, + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_display": false, + "guest_accelerator": [], + "hostname": "", + "id": "projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie", + "instance_encryption_key": [], + "instance_id": "7368592281459855388", + "key_revocation_action_type": "", + "label_fingerprint": "vezUS-42LLM=", + "labels": null, + "machine_type": "e2-micro", + "metadata": null, + "metadata_fingerprint": "jBFO10OSSew=", + "metadata_startup_script": null, + "min_cpu_platform": "", + "name": "sacred-dassie", + "network_interface": [ + { + "access_config": [ + { + "nat_ip": "35.225.226.220", + "network_tier": "PREMIUM", + "public_ptr_domain_name": "" + } + ], + "alias_ip_range": [], + "igmp_query": "", + "internal_ipv6_prefix_length": 0, + "ipv6_access_config": [], + "ipv6_access_type": "", + "ipv6_address": "", + "name": "nic0", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "network_attachment": "", + "network_ip": "10.2.0.2", + "nic_type": "", + "parent_nic_name": "", + "queue_count": 0, + "stack_type": "IPV4_ONLY", + "subnetwork": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/sacred-dassie-subnet", + "subnetwork_project": "stacklet-test-policies", + "vlan": 0 + } + ], + "network_performance_config": [], + "params": [], + "project": "stacklet-test-policies", + "reservation_affinity": [], + "resource_policies": null, + "scheduling": [ + { + "automatic_restart": true, + "availability_domain": 0, + "instance_termination_action": "", + "local_ssd_recovery_timeout": [], + "max_run_duration": [], + "min_node_cpus": 0, + "node_affinities": [], + "on_host_maintenance": "MIGRATE", + "on_instance_stop_action": [], + "preemptible": false, + "provisioning_model": "STANDARD", + "termination_time": "" + } + ], + "scratch_disk": [], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/zones/us-central1-a/instances/sacred-dassie", + "service_account": [], + "shielded_instance_config": [ + { + "enable_integrity_monitoring": true, + "enable_secure_boot": false, + "enable_vtpm": true + } + ], + "tags": null, + "tags_fingerprint": "42WmSpB8rSM=", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "timeouts": null, + "zone": "us-central1-a" + } + }, + "google_compute_network": { + "vpc": { + "auto_create_subnetworks": false, + "bgp_always_compare_med": false, + "bgp_best_path_selection_mode": "LEGACY", + "bgp_inter_region_cost": "", + "delete_bgp_always_compare_med": false, + "delete_default_routes_on_create": false, + "description": "", + "enable_ula_internal_ipv6": false, + "gateway_ipv4": "", + "id": "projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "internal_ipv6_range": "", + "mtu": 0, + "name": "sacred-dassie-vpc", + "network_firewall_policy_enforcement_order": "AFTER_CLASSIC_FIREWALL", + "network_id": "5228196046757793879", + "network_profile": "", + "numeric_id": "5228196046757793879", + "params": [], + "project": "stacklet-test-policies", + "routing_mode": "GLOBAL", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "timeouts": null + } + }, + "google_compute_subnetwork": { + "network_subnet": { + "allow_subnet_cidr_routes_overlap": false, + "creation_timestamp": "2026-03-13T08:35:53.932-07:00", + "description": "", + "external_ipv6_prefix": "", + "fingerprint": null, + "gateway_address": "10.2.0.1", + "id": "projects/cloud-custodian/regions/us-central1/subnetworks/sacred-dassie-subnet", + "internal_ipv6_prefix": "", + "ip_cidr_range": "10.2.0.0/16", + "ip_collection": null, + "ipv6_access_type": "", + "ipv6_cidr_range": "", + "ipv6_gce_endpoint": "", + "log_config": [], + "name": "sacred-dassie-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/sacred-dassie-vpc", + "params": [], + "private_ip_google_access": false, + "private_ipv6_google_access": "DISABLE_GOOGLE_ACCESS", + "project": "stacklet-test-policies", + "purpose": "PRIVATE", + "region": "us-central1", + "reserved_internal_range": "", + "role": "", + "secondary_ip_range": [], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/sacred-dassie-subnet", + "send_secondary_ip_range_if_empty": null, + "stack_type": "IPV4_ONLY", + "state": "", + "subnetwork_id": 7221859027349246006, + "timeouts": null + } + }, + "random_pet": { + "server": { + "id": "sacred-dassie", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_compute.py b/tools/c7n_gcp/tests/test_compute.py index 96933e5a949..c7263d9c165 100644 --- a/tools/c7n_gcp/tests/test_compute.py +++ b/tools/c7n_gcp/tests/test_compute.py @@ -4,8 +4,9 @@ import functools import re import time +from unittest import mock -from c7n_gcp.resources.compute import Snapshot +from c7n_gcp.resources.compute import InstanceSetIpForwarding, Snapshot from gcp_common import BaseTest, audit_event_recorder, event_data from googleapiclient.errors import HttpError from c7n_gcp.client import Session, get_default_project @@ -278,6 +279,67 @@ def test_instance_refresh(test): assert resource['labelFingerprint'] == "GHZ1Un204L0=" +@terraform('gcp_instance_set_can_ip_forward') +def test_instance_set_can_ip_forward(test, gcp_instance_set_can_ip_forward): + project_id = gcp_instance_set_can_ip_forward['google_compute_instance.default.project'] + factory = test.replay_flight_data( + 'instance-set-can-ip-forward', project_id=project_id) + + instance_name = gcp_instance_set_can_ip_forward['google_compute_instance.default.name'] + policy = test.load_policy( + { + 'name': 'gcp-instance-set-ip-forwarding', + 'resource': 'gcp.instance', + 'filters': [ + {'name': instance_name}, + { + 'type': 'value', + 'key': 'canIpForward', + 'op': 'eq', + 'value': False, + }, + ], + 'actions': [ + { + 'type': 'set-ip-forwarding', + 'enabled': True, + } + ], + }, + session_factory=factory, + ) + + # Replay serves the recorded GET regardless of what was sent, so check + # the update request itself. + with mock.patch.object( + InstanceSetIpForwarding, 'invoke_api', autospec=True, + side_effect=InstanceSetIpForwarding.invoke_api) as invoke_api: + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['name'] == instance_name + + assert invoke_api.call_count == 1 + _, _, op_name, params = invoke_api.call_args.args + assert op_name == 'update' + assert params['body']['canIpForward'] is True + assert params['body']['machineType'] == resources[0]['machineType'] + assert params['minimalAction'] == 'REFRESH' + assert params['mostDisruptiveAllowedAction'] == 'REFRESH' + + # The update is an async zonal operation; poll until it is reflected. + for _ in range(10): + instance = policy.resource_manager.get_resource({ + 'project_id': project_id, + 'resourceName': gcp_instance_set_can_ip_forward['google_compute_instance.default.id'], + 'zone': gcp_instance_set_can_ip_forward['google_compute_instance.default.zone'], + }) + if instance['canIpForward'] or not test.recording: + break + time.sleep(2) + assert instance['canIpForward'] is True + assert instance['machineType'] == resources[0]['machineType'] + + class DiskTest(BaseTest): def test_disk_query(self): From 435bfa2ae37143d3104cbc1cf69c92b8668d5ce7 Mon Sep 17 00:00:00 2001 From: scott-sixfeetup Date: Sat, 17 Oct 2026 00:52:10 -0400 Subject: [PATCH 46/65] aws - ami - add set-disabled action (#10487) --------- Co-authored-by: AJ Kerrigan Co-authored-by: Claude Opus 5.5 (1M context) --- c7n/resources/ami.py | 75 +++++++ .../ec2.DescribeImages_1.json | 194 ++++++++++++++++++ .../ec2.DisableImage_1.json | 7 + .../ec2.DisableImage_2.json | 7 + .../ec2.DescribeImages_1.json | 156 ++++++++++++++ .../ec2.EnableImage_1.json | 7 + .../ec2.EnableImage_2.json | 7 + tests/test_ami.py | 102 +++++++++ 8 files changed, 555 insertions(+) create mode 100644 tests/data/placebo/test_ami_set_disabled_disable/ec2.DescribeImages_1.json create mode 100644 tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_1.json create mode 100644 tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_2.json create mode 100644 tests/data/placebo/test_ami_set_disabled_enable/ec2.DescribeImages_1.json create mode 100644 tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_1.json create mode 100644 tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_2.json diff --git a/c7n/resources/ami.py b/c7n/resources/ami.py index 3810cfcb348..c5ae20244b8 100644 --- a/c7n/resources/ami.py +++ b/c7n/resources/ami.py @@ -10,6 +10,8 @@ from concurrent.futures import as_completed +import botocore + from c7n.actions import BaseAction from c7n.exceptions import ClientError, PolicyValidationError from c7n.filters import ( @@ -25,6 +27,7 @@ parse_date, jmespath_compile ) +from c7n.vendored.distutils.version import LooseVersion from c7n import deprecated @@ -221,6 +224,78 @@ def process(self, images): ImageId=i['ImageId'], DeprecateAt=date) +@AMI.action_registry.register('set-disabled') +class SetDisabled(BaseAction): + """Action to enable or disable an AMI's disabled state. + + Note that disabled AMIs are not returned by default in DescribeImages. To + enable disabled AMIs, you must include `IncludeDisabled: true` in the query + block. + + Requires botocore>=1.31.63 (boto3>=1.28.63). + + :example: + + Disable an AMI (set disabled state to true) + + .. code-block:: yaml + + policies: + - name: ami-disable + resource: aws.ami + filters: + - Name: LambdaCompiler + actions: + - type: set-disabled + state: true + + :example: + + Enable a disabled AMI (set disabled state to false) + + .. code-block:: yaml + + policies: + - name: ami-enable + resource: aws.ami + query: + - IncludeDisabled: true + filters: + - Name: LambdaCompiler + actions: + - type: set-disabled + state: false + """ + + schema = type_schema('set-disabled', state={'type': 'boolean'}, required=('state',)) + permissions = ('ec2:DisableImage', 'ec2:EnableImage') + + def validate(self): + botocore_min_version = '1.31.63' + if LooseVersion(botocore.__version__) < LooseVersion(botocore_min_version): + raise PolicyValidationError( + "'set-disabled' action requires botocore version " + f'{botocore_min_version} or above. ' + f'Installed version is {botocore.__version__}.' + ) + + def process(self, images): + client = local_session(self.manager.session_factory).client('ec2') + image_count = len(images) + images = self.filter_resources(images, 'OwnerId', self.manager.ctx.options.account_id) + if len(images) != image_count: + self.log.info("Implicitly filtered %d non owned images", image_count - len(images)) + + disable = self.data.get('state', False) + op = client.disable_image if disable else client.enable_image + + for i in images: + self.manager.retry( + op, + ignore_err_codes=('InvalidAMIID.NotFound',), + ImageId=i['ImageId']) + + @AMI.action_registry.register('remove-launch-permissions') class RemoveLaunchPermissions(BaseAction): """Action to remove the ability to launch an instance from an AMI diff --git a/tests/data/placebo/test_ami_set_disabled_disable/ec2.DescribeImages_1.json b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DescribeImages_1.json new file mode 100644 index 00000000000..5b95bd99338 --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DescribeImages_1.json @@ -0,0 +1,194 @@ +{ + "status_code": 200, + "data": { + "Images": [ + { + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "BlockDeviceMappings": [ + { + "Ebs": { + "DeleteOnTermination": true, + "Iops": 3000, + "SnapshotId": "snap-0a580eed1d8b98130", + "VolumeSize": 100, + "VolumeType": "gp3", + "Throughput": 125, + "Encrypted": false + }, + "DeviceName": "/dev/xvda" + } + ], + "Description": "This image is created by the AWS Backup service.", + "EnaSupport": true, + "Hypervisor": "xen", + "Name": "AwsBackup_i-0e6014ff729c395a9_705FE5A3-40DA-A8A9-772E-4293BA46D461", + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "Tags": [ + { + "Key": "automation", + "Value": "terraform" + }, + { + "Key": "aws:backup:source-resource", + "Value": "i-0e6014ff729c395a9:705FE5A3-40DA-A8A9-772E-4293BA46D461" + }, + { + "Key": "application", + "Value": "demoec2" + }, + { + "Key": "automation.config", + "Value": "demoec2.sandbox" + }, + { + "Key": "kubernetes.io/cluster/demoec2-sandbox", + "Value": "owned" + }, + { + "Key": "environment", + "Value": "sandbox" + }, + { + "Key": "Name", + "Value": "demoec2-sandbox-0" + }, + { + "Key": "cloudformation", + "Value": "this_is_a_test" + } + ], + "VirtualizationType": "hvm", + "BootMode": "uefi-preferred", + "ImdsSupport": "v2.0", + "SourceInstanceId": "i-0e6014ff729c395a9", + "DeregistrationProtection": "disabled", + "SourceImageId": "ami-0fb6158ae71f901ed", + "SourceImageRegion": "us-east-1", + "FreeTierEligible": true, + "ImageId": "ami-00415e659887728c0", + "ImageLocation": "644160558196/AwsBackup_i-0e6014ff729c395a9_705FE5A3-40DA-A8A9-772E-4293BA46D461", + "State": "available", + "OwnerId": "644160558196", + "CreationDate": "2025-12-10T07:38:23.000Z", + "Public": false, + "Architecture": "x86_64", + "ImageType": "machine" + }, + { + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "BlockDeviceMappings": [ + { + "Ebs": { + "DeleteOnTermination": true, + "Iops": 3000, + "SnapshotId": "snap-05ec50edb4aeac57b", + "VolumeSize": 100, + "VolumeType": "gp3", + "Throughput": 125, + "Encrypted": false + }, + "DeviceName": "/dev/xvda" + } + ], + "Description": "This image is created by the AWS Backup service.", + "EnaSupport": true, + "Hypervisor": "xen", + "Name": "AwsBackup_i-033753f256fa9f19c_69225124-4B19-E06F-7C19-7234654F854D", + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "Tags": [ + { + "Key": "cloudformation", + "Value": "this_is_a_test" + }, + { + "Key": "Name", + "Value": "demoec2-sandbox-0" + }, + { + "Key": "kubernetes.io/cluster/demoec2-sandbox", + "Value": "owned" + }, + { + "Key": "automation", + "Value": "terraform" + }, + { + "Key": "automation.config", + "Value": "demoec2.sandbox" + }, + { + "Key": "aws:backup:source-resource", + "Value": "i-033753f256fa9f19c:69225124-4B19-E06F-7C19-7234654F854D" + }, + { + "Key": "environment", + "Value": "sandbox" + }, + { + "Key": "application", + "Value": "demoec2" + } + ], + "VirtualizationType": "hvm", + "BootMode": "uefi-preferred", + "ImdsSupport": "v2.0", + "SourceInstanceId": "i-033753f256fa9f19c", + "DeregistrationProtection": "disabled", + "SourceImageId": "ami-0fb6158ae71f901ed", + "SourceImageRegion": "us-east-1", + "FreeTierEligible": true, + "ImageId": "ami-06902a5fc08ba032e", + "ImageLocation": "644160558196/AwsBackup_i-033753f256fa9f19c_69225124-4B19-E06F-7C19-7234654F854D", + "State": "available", + "OwnerId": "644160558196", + "CreationDate": "2025-12-12T07:31:45.000Z", + "Public": false, + "Architecture": "x86_64", + "ImageType": "machine" + }, + { + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "BlockDeviceMappings": [ + { + "Ebs": { + "DeleteOnTermination": true, + "Iops": 3000, + "SnapshotId": "snap-0123456789abcdef0", + "VolumeSize": 100, + "VolumeType": "gp3", + "Throughput": 125, + "Encrypted": false + }, + "DeviceName": "/dev/xvda" + } + ], + "Description": "AMI shared from another account", + "EnaSupport": true, + "Hypervisor": "xen", + "Name": "shared-base-image", + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "Tags": [], + "VirtualizationType": "hvm", + "BootMode": "uefi-preferred", + "ImdsSupport": "v2.0", + "DeregistrationProtection": "disabled", + "FreeTierEligible": true, + "ImageId": "ami-0a1b2c3d4e5f60718", + "ImageLocation": "123456789012/shared-base-image", + "State": "available", + "OwnerId": "123456789012", + "CreationDate": "2025-12-12T07:31:45.000Z", + "Public": false, + "Architecture": "x86_64", + "ImageType": "machine" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_1.json b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_1.json new file mode 100644 index 00000000000..3bd71f550ec --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Return": true, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_2.json b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_2.json new file mode 100644 index 00000000000..3bd71f550ec --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_disable/ec2.DisableImage_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Return": true, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ami_set_disabled_enable/ec2.DescribeImages_1.json b/tests/data/placebo/test_ami_set_disabled_enable/ec2.DescribeImages_1.json new file mode 100644 index 00000000000..b83d79b6f44 --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_enable/ec2.DescribeImages_1.json @@ -0,0 +1,156 @@ +{ + "status_code": 200, + "data": { + "Images": [ + { + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "BlockDeviceMappings": [ + { + "Ebs": { + "DeleteOnTermination": true, + "Iops": 3000, + "SnapshotId": "snap-05ec50edb4aeac57b", + "VolumeSize": 100, + "VolumeType": "gp3", + "Throughput": 125, + "Encrypted": false + }, + "DeviceName": "/dev/xvda" + } + ], + "Description": "This image is created by the AWS Backup service.", + "EnaSupport": true, + "Hypervisor": "xen", + "Name": "AwsBackup_i-033753f256fa9f19c_69225124-4B19-E06F-7C19-7234654F854D", + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "Tags": [ + { + "Key": "cloudformation", + "Value": "this_is_a_test" + }, + { + "Key": "Name", + "Value": "demoec2-sandbox-0" + }, + { + "Key": "kubernetes.io/cluster/demoec2-sandbox", + "Value": "owned" + }, + { + "Key": "automation", + "Value": "terraform" + }, + { + "Key": "automation.config", + "Value": "demoec2.sandbox" + }, + { + "Key": "aws:backup:source-resource", + "Value": "i-033753f256fa9f19c:69225124-4B19-E06F-7C19-7234654F854D" + }, + { + "Key": "environment", + "Value": "sandbox" + }, + { + "Key": "application", + "Value": "demoec2" + } + ], + "VirtualizationType": "hvm", + "BootMode": "uefi-preferred", + "ImdsSupport": "v2.0", + "SourceInstanceId": "i-033753f256fa9f19c", + "DeregistrationProtection": "disabled", + "SourceImageId": "ami-0fb6158ae71f901ed", + "SourceImageRegion": "us-east-1", + "FreeTierEligible": true, + "ImageId": "ami-06902a5fc08ba032e", + "ImageLocation": "644160558196/AwsBackup_i-033753f256fa9f19c_69225124-4B19-E06F-7C19-7234654F854D", + "State": "disabled", + "OwnerId": "644160558196", + "CreationDate": "2025-12-12T07:31:45.000Z", + "Public": false, + "Architecture": "x86_64", + "ImageType": "machine" + }, + { + "PlatformDetails": "Linux/UNIX", + "UsageOperation": "RunInstances", + "BlockDeviceMappings": [ + { + "Ebs": { + "DeleteOnTermination": true, + "Iops": 3000, + "SnapshotId": "snap-0a580eed1d8b98130", + "VolumeSize": 100, + "VolumeType": "gp3", + "Throughput": 125, + "Encrypted": false + }, + "DeviceName": "/dev/xvda" + } + ], + "Description": "This image is created by the AWS Backup service.", + "EnaSupport": true, + "Hypervisor": "xen", + "Name": "AwsBackup_i-0e6014ff729c395a9_705FE5A3-40DA-A8A9-772E-4293BA46D461", + "RootDeviceName": "/dev/xvda", + "RootDeviceType": "ebs", + "Tags": [ + { + "Key": "automation", + "Value": "terraform" + }, + { + "Key": "aws:backup:source-resource", + "Value": "i-0e6014ff729c395a9:705FE5A3-40DA-A8A9-772E-4293BA46D461" + }, + { + "Key": "application", + "Value": "demoec2" + }, + { + "Key": "automation.config", + "Value": "demoec2.sandbox" + }, + { + "Key": "kubernetes.io/cluster/demoec2-sandbox", + "Value": "owned" + }, + { + "Key": "environment", + "Value": "sandbox" + }, + { + "Key": "Name", + "Value": "demoec2-sandbox-0" + }, + { + "Key": "cloudformation", + "Value": "this_is_a_test" + } + ], + "VirtualizationType": "hvm", + "BootMode": "uefi-preferred", + "ImdsSupport": "v2.0", + "SourceInstanceId": "i-0e6014ff729c395a9", + "DeregistrationProtection": "disabled", + "SourceImageId": "ami-0fb6158ae71f901ed", + "SourceImageRegion": "us-east-1", + "FreeTierEligible": true, + "ImageId": "ami-00415e659887728c0", + "ImageLocation": "644160558196/AwsBackup_i-0e6014ff729c395a9_705FE5A3-40DA-A8A9-772E-4293BA46D461", + "State": "disabled", + "OwnerId": "644160558196", + "CreationDate": "2025-12-10T07:38:23.000Z", + "Public": false, + "Architecture": "x86_64", + "ImageType": "machine" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_1.json b/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_1.json new file mode 100644 index 00000000000..3bd71f550ec --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Return": true, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_2.json b/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_2.json new file mode 100644 index 00000000000..3bd71f550ec --- /dev/null +++ b/tests/data/placebo/test_ami_set_disabled_enable/ec2.EnableImage_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Return": true, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/test_ami.py b/tests/test_ami.py index ddb8de822f7..f4f50be9c58 100644 --- a/tests/test_ami.py +++ b/tests/test_ami.py @@ -1,11 +1,16 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from unittest import mock + from c7n.exceptions import ClientError, PolicyValidationError from c7n.resources.ami import ErrorHandler from c7n.query import DescribeSource from c7n.utils import jmespath_search +from .apicallcaptor import ApiCallCaptor from .common import BaseTest +import pytest + class TestAMI(BaseTest): @@ -472,3 +477,100 @@ def test_ami_set_permissions_remove_matched_no_add(self): ImageId=resources[0]['ImageId'], Attribute='launchPermission')['LaunchPermissions'] assert perms == [] + + def test_ami_set_disabled_disable(self): + factory = self.replay_flight_data('test_ami_set_disabled_disable') + # The last image is shared from another account and should be skipped + image_ids = ['ami-06902a5fc08ba032e', 'ami-00415e659887728c0', 'ami-0a1b2c3d4e5f60718'] + + p = self.load_policy( + { + 'name': 'ami-set-disabled-disable', + 'resource': 'aws.ami', + 'query': [{'ImageIds': image_ids}], + 'actions': [{'type': 'set-disabled', 'state': True}], + }, + session_factory=factory, + ) + p.validate() + captor = ApiCallCaptor.start_capture() + resources = p.run() + self.assertEqual(len(resources), 3) + disabled = [ + c['params']['ImageId'] for c in captor.calls if c['operation'] == 'DisableImage' + ] + self.assertEqual(sorted(disabled), ['ami-00415e659887728c0', 'ami-06902a5fc08ba032e']) + + def test_ami_set_disabled_enable(self): + factory = self.replay_flight_data('test_ami_set_disabled_enable') + image_ids = ['ami-06902a5fc08ba032e', 'ami-00415e659887728c0'] + + p = self.load_policy( + { + 'name': 'ami-set-disabled-enable', + 'resource': 'aws.ami', + 'query': [{'IncludeDisabled': True}, {'ImageIds': image_ids}], + 'actions': [{'type': 'set-disabled', 'state': False}], + }, + session_factory=factory, + ) + p.validate() + captor = ApiCallCaptor.start_capture() + resources = p.run() + self.assertEqual(len(resources), 2) + enabled = [ + c['params']['ImageId'] for c in captor.calls if c['operation'] == 'EnableImage' + ] + self.assertEqual(sorted(enabled), sorted(image_ids)) + + def test_ami_set_disabled_ignores_not_found(self): + p = self.load_policy( + { + 'name': 'ami-set-disabled-notfound', + 'resource': 'aws.ami', + 'actions': [{'type': 'set-disabled', 'state': True}], + } + ) + action = p.resource_manager.actions[0] + + error_response = {'Error': {'Code': 'InvalidAMIID.NotFound', 'Message': 'not found'}} + client = mock.MagicMock() + client.disable_image.side_effect = ClientError(error_response, 'DisableImage') + + session = mock.MagicMock() + session.client.return_value = client + + # Prefer TestUtils.patch helper (used elsewhere in the suite) over a one-off mock.patch + import c7n.resources.ami as ami + + self.patch(ami, 'local_session', lambda _: session) + action.process([{'ImageId': 'ami-11111111', 'OwnerId': self.account_id}]) + client.disable_image.assert_called_once() + + +@pytest.mark.parametrize('botocore_version', ['1.31.62', '1.30.99', '0.99.0']) +def test_ami_set_disabled_lower_botocore_version_validation(test, botocore_version): + with mock.patch('botocore.__version__', botocore_version): + with test.assertRaises(PolicyValidationError) as cm: + policy = test.load_policy( + { + 'name': 'ami-set-disabled', + 'resource': 'aws.ami', + 'actions': [{'type': 'set-disabled', 'state': True}], + } + ) + policy.validate() + test.assertIn('requires botocore version 1.31.63 or above', str(cm.exception)) + + +@pytest.mark.parametrize('botocore_version', ['1.31.63', '1.31.64', '1.32.0', '2.0.0']) +def test_ami_set_disabled_above_botocore_version_validation(test, botocore_version): + with mock.patch('botocore.__version__', botocore_version): + policy = test.load_policy( + { + 'name': 'ami-set-disabled', + 'resource': 'aws.ami', + 'actions': [{'type': 'set-disabled', 'state': True}], + } + ) + policy.validate() From 9631a4b80f124b203269eda94da5b8f3a2efcf6f Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Sat, 17 Oct 2026 00:54:39 -0400 Subject: [PATCH 47/65] azure - add machine-learning-schedule resource (#11214) --- tools/c7n_azure/c7n_azure/resources/arm.py | 3 +- .../resources/machine_learning_schedule.py | 79 +++++ .../c7n_azure/resources/resource_map.py | 1 + ...heduleTest.machine-learning-schedules.json | 294 ++++++++++++++++++ .../templates/machine-learning-schedule.json | 126 ++++++++ .../test_machine_learning_schedule.py | 171 ++++++++++ 6 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 tools/c7n_azure/c7n_azure/resources/machine_learning_schedule.py create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningScheduleTest.machine-learning-schedules.json create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-schedule.json create mode 100644 tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_schedule.py diff --git a/tools/c7n_azure/c7n_azure/resources/arm.py b/tools/c7n_azure/c7n_azure/resources/arm.py index 3c38c5c90e6..ff191b19387 100644 --- a/tools/c7n_azure/c7n_azure/resources/arm.py +++ b/tools/c7n_azure/c7n_azure/resources/arm.py @@ -26,7 +26,8 @@ 'microsoft.storage/storageaccounts/blobservices/containers', 'microsoft.cognitiveservices/accounts/deployments', 'microsoft.cognitiveservices/accounts/projects/connections', - 'microsoft.machinelearningservices/workspaces/jobs'] + 'microsoft.machinelearningservices/workspaces/jobs', + 'microsoft.machinelearningservices/workspaces/schedules'] class ArmTypeInfo(TypeInfo, metaclass=TypeMeta): diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_schedule.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_schedule.py new file mode 100644 index 00000000000..16e6cf54bdd --- /dev/null +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_schedule.py @@ -0,0 +1,79 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +import logging + +from azure.core.exceptions import ResourceNotFoundError +from c7n_azure.provider import resources +from c7n_azure.resources.arm import ChildArmResourceManager + +log = logging.getLogger('custodian.azure.machine_learning') + + +@resources.register('machine-learning-schedule') +class MachineLearningSchedule(ChildArmResourceManager): + """Azure Machine Learning Schedule Resource + + Schedules are child resources of a Machine Learning workspace + (``Microsoft.MachineLearningServices/workspaces/schedules``) that + trigger a job or batch endpoint on a recurrence or cron trigger. + Disabled schedules are included. + + :example: + + Find enabled schedules that create a job every minute or hour. + + .. code-block:: yaml + + policies: + - name: ml-schedules-sub-daily-jobs + resource: azure.machine-learning-schedule + filters: + - type: value + key: properties.isEnabled + value: true + - type: value + key: properties.action.actionType + value: CreateJob + - type: value + key: properties.trigger.triggerType + value: Recurrence + - type: value + key: properties.trigger.frequency + op: in + value: + - Minute + - Hour + + """ + + class resource_type(ChildArmResourceManager.resource_type): + doc_groups = ['AI + Machine Learning'] + service = 'azure.mgmt.machinelearningservices' + client = 'MachineLearningServicesMgmtClient' + enum_spec = ('schedules', 'list', None) + parent_manager_name = 'machine-learning-workspace' + resource_type = 'Microsoft.MachineLearningServices/workspaces/schedules' + diagnostic_settings_enabled = False + default_report_fields = ( + 'name', + 'resourceGroup', + '"c7n:parent-id"' + ) + + @classmethod + def extra_args(cls, parent_resource): + return { + 'resource_group_name': parent_resource['resourceGroup'], + 'workspace_name': parent_resource['name'], + 'list_view_type': 'All', + } + + def enumerate_resources(self, parent_resource, type_info, vault_url=None, **params): + try: + return super().enumerate_resources( + parent_resource, type_info, vault_url=vault_url, **params) + except ResourceNotFoundError as e: + # A workspace stuck deleting is still listed after its resource group is gone. + log.warning('Skipping Machine Learning workspace %s: %s', parent_resource['id'], e) + return [] diff --git a/tools/c7n_azure/c7n_azure/resources/resource_map.py b/tools/c7n_azure/c7n_azure/resources/resource_map.py index dcb1e854d35..70c21a2cb23 100644 --- a/tools/c7n_azure/c7n_azure/resources/resource_map.py +++ b/tools/c7n_azure/c7n_azure/resources/resource_map.py @@ -74,6 +74,7 @@ "azure.machine-learning-job": "c7n_azure.resources.machine_learning_job.MachineLearningJob", "azure.machine-learning-model-version": "c7n_azure.resources.machine_learning_model_version.MachineLearningModelVersion", # noqa "azure.machine-learning-online-endpoint": "c7n_azure.resources.machine_learning_online_endpoint.MachineLearningOnlineEndpoint", # noqa + "azure.machine-learning-schedule": "c7n_azure.resources.machine_learning_schedule.MachineLearningSchedule", # noqa "azure.mariadb": "c7n_azure.resources.mariadb.MariaDB", "azure.monitor-log-profile": "c7n_azure.resources.monitor_logprofile.MonitorLogprofile", "azure.mariadb-server": "c7n_azure.resources.mariadb_server.MariaDBServer", diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningScheduleTest.machine-learning-schedules.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningScheduleTest.machine-learning-schedules.json new file mode 100644 index 00000000000..cf4980ecce9 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningScheduleTest.machine-learning-schedules.json @@ -0,0 +1,294 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "cache-control": [ + "no-cache" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Thu, 15 Oct 2026 20:31:17 GMT" + ], + "x-msedge-ref": [ + "Ref A: 22B2167BB30944E2A33225A38455CBA5 Ref B: CHI261140503025 Ref C: 2026-10-15T20:31:17Z" + ], + "content-length": [ + "15497" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319", + "name": "cctest-mlws-129259718319", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "eastus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.Storage/storageAccounts/cctestml129259718319", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.Keyvault/vaults/cctestmlkv129259718319", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.insights/components/cctestmlai129259718319", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-10-15T20:29:26.4599550Z", + "notebookInfo": { + "resourceId": "ef5452aef35c42ff84b691117ac1d605", + "fqdn": "ml-cctest-mlws-1292-eastus-00000000-0000-0000-0000-000000000005.eastus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "00000000-0000-0000-0000-000000000005", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://eastus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://eastus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "00000000-0000-0000-0000-000000000002", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-10-15T20:29:26.459955Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-10-15T20:29:26.459955Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319/schedules?api-version=2023-04-01&listViewType=All", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "request-context": [ + "appId=cid-v1:2d2e8e63-272e-4b3c-8598-4ee570a0e70d" + ], + "content-length": [ + "4505" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: F37F6FEC51BE4DBEA48DD1F2337282F0 Ref B: CH1AA2020610049 Ref C: 2026-10-15T20:31:20Z" + ], + "x-ms-response-type": [ + "standard" + ], + "azureml-served-by-cluster": [ + "vienna-eastus-01" + ], + "date": [ + "Thu, 15 Oct 2026 20:31:20 GMT" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "cache-control": [ + "no-cache" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-request-time": [ + "0.140" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319/schedules/cctest-schedule-minute", + "name": "cctest-schedule-minute", + "properties": { + "description": null, + "tags": null, + "properties": null, + "displayName": null, + "isEnabled": false, + "trigger": { + "endTime": null, + "startTime": "2026-10-15T20:30:37", + "timeZone": "UTC", + "triggerType": "Recurrence", + "frequency": "Minute", + "interval": 1, + "schedule": null + }, + "action": { + "actionType": "CreateJob", + "jobDefinition": { + "description": null, + "tags": null, + "properties": null, + "displayName": null, + "status": "NotStarted", + "experimentName": "Default", + "services": {}, + "computeId": null, + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319/environments/cctest-schedule-environment/versions/1", + "inputs": {}, + "outputs": {}, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": null + } + }, + "provisioningState": "Succeeded" + }, + "systemData": { + "createdAt": "2026-10-15T20:30:27.9843568+00:00", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-10-15T20:30:27.9843568+00:00", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319/schedules/cctest-schedule-yearly", + "name": "cctest-schedule-yearly", + "properties": { + "description": null, + "tags": null, + "properties": null, + "displayName": null, + "isEnabled": true, + "trigger": { + "endTime": null, + "startTime": "2026-10-15T20:30:36", + "timeZone": "UTC", + "triggerType": "Cron", + "expression": "0 0 1 1 *" + }, + "action": { + "actionType": "CreateJob", + "jobDefinition": { + "description": null, + "tags": null, + "properties": null, + "displayName": null, + "status": "NotStarted", + "experimentName": "Default", + "services": {}, + "computeId": null, + "isArchived": false, + "identity": null, + "componentId": null, + "jobType": "Command", + "resources": { + "instanceCount": 1, + "instanceType": null, + "properties": null, + "shmSize": "2g", + "dockerArgs": null + }, + "codeId": null, + "command": "echo hello", + "environmentId": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-schedule/providers/Microsoft.MachineLearningServices/workspaces/cctest-mlws-129259718319/environments/cctest-schedule-environment/versions/1", + "inputs": {}, + "outputs": {}, + "distribution": null, + "limits": null, + "environmentVariables": null, + "parameters": null + } + }, + "provisioningState": "Succeeded" + }, + "systemData": { + "createdAt": "2026-10-15T20:30:21.9389572+00:00", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-10-15T20:30:21.9389572+00:00", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + } + ] +} diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-schedule.json b/tools/c7n_azure/tests_azure/templates/machine-learning-schedule.json new file mode 100644 index 00000000000..44755bde39e --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-schedule.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + } + }, + "variables": { + "workspaceName": "[concat('cctest-mlws-', parameters('uniqueId'))]", + "storageAccountName": "[concat('cctestml', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestmlkv', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestmlai', parameters('uniqueId'))]", + "environmentName": "cctest-schedule-environment", + "jobDefinition": { + "jobType": "Command", + "command": "echo hello", + "environmentId": "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]" + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-09-01", + "name": "[variables('storageAccountName')]", + "location": "eastus", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-10-01", + "name": "[variables('keyVaultName')]", + "location": "eastus", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [] + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "eastus", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "eastus", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/environments/versions", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}/{2}', variables('workspaceName'), variables('environmentName'), '1')]", + "properties": { + "image": "mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu22.04:latest" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/schedules", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), 'cctest-schedule-minute')]", + "properties": { + "isEnabled": false, + "trigger": { + "triggerType": "Recurrence", + "frequency": "Minute", + "interval": 1 + }, + "action": { + "actionType": "CreateJob", + "jobDefinition": "[variables('jobDefinition')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/schedules", + "apiVersion": "2023-04-01", + "name": "[format('{0}/{1}', variables('workspaceName'), 'cctest-schedule-yearly')]", + "properties": { + "isEnabled": true, + "trigger": { + "triggerType": "Cron", + "expression": "0 0 1 1 *" + }, + "action": { + "actionType": "CreateJob", + "jobDefinition": "[variables('jobDefinition')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces/environments/versions', variables('workspaceName'), variables('environmentName'), '1')]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_schedule.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_schedule.py new file mode 100644 index 00000000000..f744b812c65 --- /dev/null +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_schedule.py @@ -0,0 +1,171 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 + +from unittest.mock import Mock + +import pytest +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError +from azure.mgmt.machinelearningservices.models import ( + CommandJob, + JobScheduleAction, + RecurrenceTrigger, + Schedule, + ScheduleProperties, +) +from c7n_azure.resources.machine_learning_schedule import MachineLearningSchedule + +from ..azure_common import BaseTest, arm_template, cassette_name + +RESOURCE_GROUP = 'test_machine-learning-schedule' + +WORKSPACE_ID = ( + '/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/VV' + '/providers/Microsoft.MachineLearningServices/workspaces/{}' +) + + +class MachineLearningScheduleTest(BaseTest): + + recording_resource_group = RESOURCE_GROUP + + def test_machine_learning_schedule_query_args(self): + parent = { + 'name': 'workspace', + 'resourceGroup': 'resource-group', + } + + assert MachineLearningSchedule.resource_type.extra_args(parent) == { + 'resource_group_name': 'resource-group', + 'workspace_name': 'workspace', + 'list_view_type': 'All', + } + + def test_machine_learning_schedule_schema_validate(self): + with self.sign_out_patch(): + p = self.load_policy({ + 'name': 'find-sub-daily-machine-learning-schedules', + 'resource': 'azure.machine-learning-schedule', + 'filters': [ + {'type': 'value', 'key': 'properties.isEnabled', 'value': True}, + { + 'type': 'value', + 'key': 'properties.trigger.frequency', + 'op': 'in', + 'value': ['Minute', 'Hour'], + }, + ], + }, validate=True) + assert p + + for action in ('tag', 'untag', 'auto-tag-user', 'auto-tag-date', + 'tag-trim', 'mark-for-op'): + assert action not in MachineLearningSchedule.action_registry + assert 'marked-for-op' not in MachineLearningSchedule.filter_registry + assert 'diagnostic-settings' not in MachineLearningSchedule.filter_registry + + @arm_template('machine-learning-schedule.json') + @cassette_name('machine-learning-schedules') + def test_machine_learning_schedule_query(self): + p = self.load_policy({ + 'name': 'find-all-machine-learning-schedules', + 'resource': 'azure.machine-learning-schedule', + }) + resources = p.run() + + assert {r['name']: r['properties']['isEnabled'] for r in resources} == { + 'cctest-schedule-minute': False, + 'cctest-schedule-yearly': True, + } + minute = next(r for r in resources if r['name'] == 'cctest-schedule-minute') + assert minute['properties']['trigger']['triggerType'] == 'Recurrence' + assert minute['properties']['trigger']['frequency'] == 'Minute' + assert minute['properties']['trigger']['interval'] == 1 + + yearly = next(r for r in resources if r['name'] == 'cctest-schedule-yearly') + assert yearly['properties']['trigger']['triggerType'] == 'Cron' + assert yearly['properties']['trigger']['expression'] == '0 0 1 1 *' + + for r in resources: + assert r['properties']['action']['actionType'] == 'CreateJob' + assert r['id'].lower().startswith(r['c7n:parent-id'].lower() + '/schedules/') + + @arm_template('machine-learning-schedule.json') + @cassette_name('machine-learning-schedules') + def test_machine_learning_schedule_filter(self): + p = self.load_policy({ + 'name': 'find-disabled-minute-machine-learning-schedules', + 'resource': 'azure.machine-learning-schedule', + 'filters': [ + {'type': 'value', 'key': 'properties.isEnabled', 'value': False}, + {'type': 'value', 'key': 'properties.action.actionType', 'value': 'CreateJob'}, + {'type': 'value', 'key': 'properties.trigger.triggerType', 'value': 'Recurrence'}, + { + 'type': 'value', + 'key': 'properties.trigger.frequency', + 'op': 'in', + 'value': ['Minute', 'Hour'], + }, + ], + }) + resources = p.run() + + assert [r['name'] for r in resources] == ['cctest-schedule-minute'] + + def test_machine_learning_schedule_skips_missing_workspace(self): + schedule = Schedule( + properties=ScheduleProperties( + action=JobScheduleAction( + job_definition=CommandJob(command='echo hello', environment_id='env') + ), + trigger=RecurrenceTrigger(frequency='Minute', interval=1), + is_enabled=True, + ) + ) + schedule.id = WORKSPACE_ID.format('live') + '/schedules/every-minute' + schedule.name = 'every-minute' + + def list_schedules(workspace_name, **kwargs): + if workspace_name == 'gone': + raise ResourceNotFoundError('ParentResourceNotFound') + return [schedule] + + client = Mock() + client.schedules.list.side_effect = list_schedules + + resources = self._manager(client, ['gone', 'live']).resources() + + assert [r['id'] for r in resources] == [schedule.id] + assert resources[0]['c7n:parent-id'] == WORKSPACE_ID.format('live') + assert resources[0]['properties']['trigger']['frequency'] == 'Minute' + client.schedules.list.assert_any_call( + resource_group_name='VV', + workspace_name='live', + list_view_type='All', + ) + + def test_machine_learning_schedule_raises_other_errors(self): + client = Mock() + client.schedules.list.side_effect = HttpResponseError('Forbidden') + + manager = self._manager(client, ['live']) + + with pytest.raises(HttpResponseError): + manager.resources() + + def _manager(self, client, workspace_names): + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [{ + 'id': WORKSPACE_ID.format(name), + 'name': name, + 'resourceGroup': 'VV', + } for name in workspace_names] + + p = self.load_policy({ + 'name': 'find-all-machine-learning-schedules', + 'resource': 'azure.machine-learning-schedule', + }) + manager = p.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + manager.get_client = Mock(return_value=client) + return manager From bb5cd9528b9990f2910c16638201bd86bb90b308 Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Sat, 17 Oct 2026 00:59:16 -0400 Subject: [PATCH 48/65] aws - sagemaker-space - add update action (#11212) --- c7n/resources/sagemaker.py | 100 +++- .../api.sagemaker.DescribeSpace_1.json | 39 ++ .../api.sagemaker.DescribeSpace_2.json | 55 +++ .../api.sagemaker.DescribeSpace_3.json | 50 ++ .../api.sagemaker.DescribeSpace_4.json | 50 ++ .../api.sagemaker.DescribeSpace_5.json | 55 +++ .../api.sagemaker.DescribeSpace_6.json | 39 ++ .../api.sagemaker.DescribeSpace_7.json | 50 ++ .../api.sagemaker.ListSpaces_1.json | 151 +++++++ .../api.sagemaker.UpdateSpace_1.json | 7 + .../api.sagemaker.UpdateSpace_2.json | 7 + .../tagging.GetResources_1.json | 8 + .../api.sagemaker.DescribeSpace_1.json | 39 ++ .../api.sagemaker.DescribeSpace_2.json | 55 +++ .../api.sagemaker.DescribeSpace_3.json | 50 ++ .../api.sagemaker.DescribeSpace_4.json | 50 ++ .../api.sagemaker.DescribeSpace_5.json | 50 ++ .../api.sagemaker.ListSpaces_1.json | 151 +++++++ .../api.sagemaker.UpdateSpace_1.json | 10 + .../api.sagemaker.UpdateSpace_2.json | 7 + .../tagging.GetResources_1.json | 8 + .../terraform/sagemaker_space_update/main.tf | 155 +++++++ .../sagemaker_space_update/tf_resources.json | 427 ++++++++++++++++++ tests/test_sagemaker.py | 175 ++++++- 24 files changed, 1786 insertions(+), 2 deletions(-) create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_2.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_3.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_4.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_5.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_6.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_7.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.ListSpaces_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_2.json create mode 100644 tests/data/placebo/test_sagemaker_space_update/tagging.GetResources_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_2.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_3.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_4.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_5.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.ListSpaces_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_1.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_2.json create mode 100644 tests/data/placebo/test_sagemaker_space_update_error/tagging.GetResources_1.json create mode 100644 tests/terraform/sagemaker_space_update/main.tf create mode 100644 tests/terraform/sagemaker_space_update/tf_resources.json diff --git a/c7n/resources/sagemaker.py b/c7n/resources/sagemaker.py index c29acb6c8c1..3ae7138365b 100644 --- a/c7n/resources/sagemaker.py +++ b/c7n/resources/sagemaker.py @@ -9,9 +9,10 @@ import yaml from c7n.actions import BaseAction -from c7n.exceptions import PolicyValidationError +from c7n.exceptions import PolicyExecutionError, PolicyValidationError from c7n.manager import resources from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, ConfigSource +from c7n.resources.aws import shape_schema, shape_validate from c7n.utils import local_session, type_schema, QueryParser from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment from c7n.filters.vpc import SubnetFilter, SecurityGroupFilter, NetworkLocation @@ -1799,6 +1800,103 @@ class resource_type(TypeInfo): source_mapping = {'describe': SagemakerSpaceDescribe} +@SagemakerSpace.action_registry.register('update') +class UpdateSagemakerSpace(BaseAction): + """Update a SageMaker Studio space's settings with ``UpdateSpace``. + + Options are the ``UpdateSpace`` request without ``DomainId`` and + ``SpaceName``, which come from each space. SageMaker merges what you send + into the space's current settings, so anything you leave out is kept. + + App settings blocks (``JupyterLabAppSettings``, ``CodeEditorAppSettings``, + ``KernelGatewayAppSettings`` and ``JupyterServerAppSettings``) are only sent + to spaces that already have them. SageMaker rejects some blocks on some + spaces, which fails the whole update, and silently adds others. Only spaces + in the ``InService`` state are updated. + + A new default instance type doesn't change an app that is already running. + It applies the next time the app starts. + + :example: + + Reset spaces whose JupyterLab default is a GPU instance to a CPU one: + + .. code-block:: yaml + + policies: + - name: sagemaker-space-cpu-default + resource: aws.sagemaker-space + filters: + - type: value + key: SpaceSettings.JupyterLabAppSettings.DefaultResourceSpec.InstanceType + op: glob + value: 'ml.g*' + actions: + - type: update + SpaceSettings: + JupyterLabAppSettings: + DefaultResourceSpec: + InstanceType: ml.t3.medium + """ + + schema = type_schema( + 'update', + **shape_schema( + 'sagemaker', 'UpdateSpaceRequest', drop_fields=('DomainId', 'SpaceName')), + ) + permissions = ('sagemaker:UpdateSpace',) + shape = 'UpdateSpaceRequest' + valid_origin_states = ('InService',) + app_settings = ( + 'JupyterServerAppSettings', + 'KernelGatewayAppSettings', + 'CodeEditorAppSettings', + 'JupyterLabAppSettings', + ) + + def validate(self): + params = {k: v for k, v in self.data.items() if k != 'type'} + params.update(DomainId='validate', SpaceName='validate') + shape_validate(params, self.shape, self.manager.resource_type.service) + return self + + def process(self, resources): + resources = self.filter_resources(resources, 'Status', self.valid_origin_states) + client = local_session(self.manager.session_factory).client('sagemaker') + skipped = errors = 0 + for r in resources: + update = self.fit_to_space(r) + if not update: + skipped += 1 + continue + try: + self.manager.retry( + client.update_space, + DomainId=r['DomainId'], SpaceName=r['SpaceName'], **update) + except client.exceptions.ResourceNotFound: + continue + except Exception: + self.log.exception("Unable to update space %s", r['SpaceArn']) + errors += 1 + if skipped: + self.log.warning( + "Skipped %d of %d spaces with none of the policy's app settings", + skipped, len(resources)) + if errors: + raise PolicyExecutionError( + "%d of %d spaces failed to update" % (errors, len(resources))) + + def fit_to_space(self, space): + update = {k: v for k, v in self.data.items() if k != 'type'} + current = space.get('SpaceSettings', {}) + settings = { + k: v for k, v in update.pop('SpaceSettings', {}).items() + if k not in self.app_settings or k in current} + if settings: + update['SpaceSettings'] = settings + return update + + class SagemakerAppDescribe(DescribeSource): def augment(self, resources): diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_1.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_1.json new file mode 100644 index 00000000000..aaaa3fd1756 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_1.json @@ -0,0 +1,39 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "SpaceName": "c7n-classic-selected-quail", + "HomeEfsFileSystemUid": "200010", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 37, + "microsecond": 417000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 35, + "microsecond": 348000 + }, + "SpaceSettings": { + "KernelGatewayAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + } + }, + "Url": "https://000000000000001.studio.us-east-1.sagemaker.aws", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_2.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_2.json new file mode 100644 index 00000000000..344e19c2f6d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_2.json @@ -0,0 +1,55 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "SpaceName": "c7n-jupyterlab-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 33, + "microsecond": 624000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 28, + "microsecond": 534000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000004.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_3.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_3.json new file mode 100644 index 00000000000..b1c257bb0db --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_3.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "SpaceName": "c7n-cpu-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 808000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 735000 + }, + "SpaceSettings": { + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.large" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000003.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_4.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_4.json new file mode 100644 index 00000000000..e8c18b3c433 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_4.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-codeeditor-selected-quail", + "SpaceName": "c7n-codeeditor-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 729000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 621000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "AppType": "CodeEditor", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000002.studio.us-east-1.sagemaker.aws/codeeditor/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_5.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_5.json new file mode 100644 index 00000000000..51d9a4f43c3 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_5.json @@ -0,0 +1,55 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "SpaceName": "c7n-jupyterlab-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 41, + "microsecond": 940000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 28, + "microsecond": 534000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.medium" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000004.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_6.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_6.json new file mode 100644 index 00000000000..70368cb4bad --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_6.json @@ -0,0 +1,39 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "SpaceName": "c7n-classic-selected-quail", + "HomeEfsFileSystemUid": "200010", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 40, + "microsecond": 395000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 35, + "microsecond": 348000 + }, + "SpaceSettings": { + "KernelGatewayAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.medium" + } + } + }, + "Url": "https://000000000000001.studio.us-east-1.sagemaker.aws", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_7.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_7.json new file mode 100644 index 00000000000..e8c18b3c433 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.DescribeSpace_7.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-codeeditor-selected-quail", + "SpaceName": "c7n-codeeditor-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 729000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 621000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "AppType": "CodeEditor", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000002.studio.us-east-1.sagemaker.aws/codeeditor/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.ListSpaces_1.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.ListSpaces_1.json new file mode 100644 index 00000000000..f2735f47828 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.ListSpaces_1.json @@ -0,0 +1,151 @@ +{ + "status_code": 200, + "data": { + "Spaces": [ + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-classic-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 35, + "microsecond": 348000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 37, + "microsecond": 417000 + }, + "SpaceSettingsSummary": {} + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-jupyterlab-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 28, + "microsecond": 534000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 33, + "microsecond": 624000 + }, + "SpaceSettingsSummary": { + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-cpu-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 735000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 808000 + }, + "SpaceSettingsSummary": { + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-codeeditor-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 621000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 729000 + }, + "SpaceSettingsSummary": { + "AppType": "CodeEditor", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_1.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_1.json new file mode 100644 index 00000000000..1912da81fba --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_2.json b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_2.json new file mode 100644 index 00000000000..f657d6a9d6b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/api.sagemaker.UpdateSpace_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update/tagging.GetResources_1.json b/tests/data/placebo/test_sagemaker_space_update/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_1.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_1.json new file mode 100644 index 00000000000..70368cb4bad --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_1.json @@ -0,0 +1,39 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "SpaceName": "c7n-classic-selected-quail", + "HomeEfsFileSystemUid": "200010", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 40, + "microsecond": 395000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 35, + "microsecond": 348000 + }, + "SpaceSettings": { + "KernelGatewayAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.medium" + } + } + }, + "Url": "https://000000000000001.studio.us-east-1.sagemaker.aws", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_2.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_2.json new file mode 100644 index 00000000000..51d9a4f43c3 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_2.json @@ -0,0 +1,55 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "SpaceName": "c7n-jupyterlab-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 41, + "microsecond": 940000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 28, + "microsecond": 534000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.medium" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000004.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_3.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_3.json new file mode 100644 index 00000000000..b1c257bb0db --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_3.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "SpaceName": "c7n-cpu-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 808000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 735000 + }, + "SpaceSettings": { + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.large" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000003.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_4.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_4.json new file mode 100644 index 00000000000..e8c18b3c433 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_4.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-codeeditor-selected-quail", + "SpaceName": "c7n-codeeditor-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 729000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 621000 + }, + "SpaceSettings": { + "CodeEditorAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.g4dn.xlarge" + } + }, + "AppType": "CodeEditor", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000002.studio.us-east-1.sagemaker.aws/codeeditor/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_5.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_5.json new file mode 100644 index 00000000000..1ad7452642a --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.DescribeSpace_5.json @@ -0,0 +1,50 @@ +{ + "status_code": 200, + "data": { + "DomainId": "d-000000000001", + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "SpaceName": "c7n-cpu-selected-quail", + "Status": "InService", + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 5, + "second": 14, + "microsecond": 123000 + }, + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 735000 + }, + "SpaceSettings": { + "JupyterLabAppSettings": { + "DefaultResourceSpec": { + "InstanceType": "ml.t3.xlarge" + } + }, + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "OwnershipSettings": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + }, + "SpaceSharingSettings": { + "SharingType": "Private" + }, + "Url": "https://000000000000003.studio.us-east-1.sagemaker.aws/jupyterlab/default", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.ListSpaces_1.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.ListSpaces_1.json new file mode 100644 index 00000000000..93f8e8fa622 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.ListSpaces_1.json @@ -0,0 +1,151 @@ +{ + "status_code": 200, + "data": { + "Spaces": [ + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-classic-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 35, + "microsecond": 348000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 40, + "microsecond": 395000 + }, + "SpaceSettingsSummary": {} + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-jupyterlab-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 28, + "microsecond": 534000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 41, + "microsecond": 940000 + }, + "SpaceSettingsSummary": { + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-cpu-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 735000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 808000 + }, + "SpaceSettingsSummary": { + "AppType": "JupyterLab", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + }, + { + "DomainId": "d-000000000001", + "SpaceName": "c7n-codeeditor-selected-quail", + "Status": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 26, + "microsecond": 621000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 15, + "hour": 16, + "minute": 4, + "second": 31, + "microsecond": 729000 + }, + "SpaceSettingsSummary": { + "AppType": "CodeEditor", + "SpaceStorageSettings": { + "EbsStorageSettings": { + "EbsVolumeSizeInGb": 5 + } + } + }, + "SpaceSharingSettingsSummary": { + "SharingType": "Private" + }, + "OwnershipSettingsSummary": { + "OwnerUserProfileName": "c7n-owner-selected-quail" + } + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_1.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_1.json new file mode 100644 index 00000000000..325199c6b61 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_1.json @@ -0,0 +1,10 @@ +{ + "status_code": 400, + "data": { + "Error": { + "Message": "Updating AppType for Space is not supported.", + "Code": "ValidationException" + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_2.json b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_2.json new file mode 100644 index 00000000000..b0ad824ad5d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/api.sagemaker.UpdateSpace_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "SpaceArn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_space_update_error/tagging.GetResources_1.json b/tests/data/placebo/test_sagemaker_space_update_error/tagging.GetResources_1.json new file mode 100644 index 00000000000..8b704d1852a --- /dev/null +++ b/tests/data/placebo/test_sagemaker_space_update_error/tagging.GetResources_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "PaginationToken": "", + "ResourceTagMappingList": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/sagemaker_space_update/main.tf b/tests/terraform/sagemaker_space_update/main.tf new file mode 100644 index 00000000000..9573dac4f76 --- /dev/null +++ b/tests/terraform/sagemaker_space_update/main.tf @@ -0,0 +1,155 @@ +provider "aws" {} + +resource "random_pet" "space" { + length = 2 + separator = "-" +} + +data "aws_vpc" "default" { + default = true +} + +data "aws_subnets" "default" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["sagemaker.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "execution" { + name = "c7n-space-update-${random_pet.space.id}" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_sagemaker_domain" "main" { + domain_name = "c7n-space-update-${random_pet.space.id}" + auth_mode = "IAM" + vpc_id = data.aws_vpc.default.id + subnet_ids = [data.aws_subnets.default.ids[0]] + + default_user_settings { + execution_role = aws_iam_role.execution.arn + } + + default_space_settings { + execution_role = aws_iam_role.execution.arn + } + + retention_policy { + home_efs_file_system = "Delete" + } +} + +resource "aws_sagemaker_user_profile" "owner" { + domain_id = aws_sagemaker_domain.main.id + user_profile_name = "c7n-owner-${random_pet.space.id}" +} + +# GPU JupyterLab and Code Editor defaults on a private space. +resource "aws_sagemaker_space" "jupyterlab" { + domain_id = aws_sagemaker_domain.main.id + space_name = "c7n-jupyterlab-${random_pet.space.id}" + + ownership_settings { + owner_user_profile_name = aws_sagemaker_user_profile.owner.user_profile_name + } + + space_sharing_settings { + sharing_type = "Private" + } + + space_settings { + app_type = "JupyterLab" + + jupyter_lab_app_settings { + default_resource_spec { + instance_type = "ml.g4dn.xlarge" + } + } + + code_editor_app_settings { + default_resource_spec { + instance_type = "ml.g4dn.xlarge" + } + } + } +} + +# A Code Editor space with a GPU default. +resource "aws_sagemaker_space" "codeeditor" { + domain_id = aws_sagemaker_domain.main.id + space_name = "c7n-codeeditor-${random_pet.space.id}" + + ownership_settings { + owner_user_profile_name = aws_sagemaker_user_profile.owner.user_profile_name + } + + space_sharing_settings { + sharing_type = "Private" + } + + space_settings { + app_type = "CodeEditor" + + code_editor_app_settings { + default_resource_spec { + instance_type = "ml.g4dn.xlarge" + } + } + } +} + +# A Studio Classic space: no owner, GPU KernelGateway default. Created last, +# so ListSpaces returns it first. +resource "aws_sagemaker_space" "classic" { + domain_id = aws_sagemaker_domain.main.id + space_name = "c7n-classic-${random_pet.space.id}" + + depends_on = [ + aws_sagemaker_space.jupyterlab, + aws_sagemaker_space.codeeditor, + aws_sagemaker_space.cpu, + ] + + space_settings { + kernel_gateway_app_settings { + default_resource_spec { + instance_type = "ml.g4dn.xlarge" + } + } + } +} + +# Already on a CPU default. +resource "aws_sagemaker_space" "cpu" { + domain_id = aws_sagemaker_domain.main.id + space_name = "c7n-cpu-${random_pet.space.id}" + + ownership_settings { + owner_user_profile_name = aws_sagemaker_user_profile.owner.user_profile_name + } + + space_sharing_settings { + sharing_type = "Private" + } + + space_settings { + app_type = "JupyterLab" + + jupyter_lab_app_settings { + default_resource_spec { + instance_type = "ml.t3.large" + } + } + } +} diff --git a/tests/terraform/sagemaker_space_update/tf_resources.json b/tests/terraform/sagemaker_space_update/tf_resources.json new file mode 100644 index 00000000000..b716b103836 --- /dev/null +++ b/tests/terraform/sagemaker_space_update/tf_resources.json @@ -0,0 +1,427 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "aws_iam_policy_document": { + "assume_role": { + "id": "1147345228", + "json": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Action\": \"sts:AssumeRole\",\n \"Principal\": {\n \"Service\": \"sagemaker.amazonaws.com\"\n }\n }\n ]\n}", + "minified_json": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}]}", + "override_json": null, + "override_policy_documents": null, + "policy_id": null, + "source_json": null, + "source_policy_documents": null, + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "condition": [], + "effect": "Allow", + "not_actions": [], + "not_principals": [], + "not_resources": [], + "principals": [ + { + "identifiers": [ + "sagemaker.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [], + "sid": "" + } + ], + "version": "2012-10-17" + } + }, + "aws_subnets": { + "default": { + "filter": [ + { + "name": "vpc-id", + "values": [ + "vpc-00000000000000001" + ] + } + ], + "id": "us-east-1", + "ids": [ + "subnet-00000000000000001", + "subnet-00000000000000002", + "subnet-00000000000000003", + "subnet-00000000000000004", + "subnet-00000000000000005", + "subnet-00000000000000006" + ], + "region": "us-east-1", + "tags": null, + "timeouts": null + } + }, + "aws_vpc": { + "default": { + "arn": "arn:aws:ec2:us-east-1:644160558196:vpc/vpc-00000000000000001", + "cidr_block": "172.31.0.0/16", + "cidr_block_associations": [ + { + "association_id": "vpc-cidr-assoc-00000000000000001", + "cidr_block": "172.31.0.0/16", + "state": "associated" + } + ], + "default": true, + "dhcp_options_id": "dopt-00000000000000001", + "enable_dns_hostnames": true, + "enable_dns_support": true, + "enable_network_address_usage_metrics": false, + "filter": null, + "id": "vpc-00000000000000001", + "instance_tenancy": "default", + "ipv6_association_id": "", + "ipv6_cidr_block": "", + "ipv6_cidr_block_associations": [], + "main_route_table_id": "rtb-00000000000000001", + "owner_id": "644160558196", + "region": "us-east-1", + "state": null, + "tags": {}, + "timeouts": null + } + }, + "aws_iam_role": { + "execution": { + "arn": "arn:aws:iam::644160558196:role/c7n-space-update-selected-quail", + "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}],\"Version\":\"2012-10-17\"}", + "create_date": "2026-10-15T20:02:28Z", + "description": "", + "force_detach_policies": false, + "id": "c7n-space-update-selected-quail", + "inline_policy": [], + "managed_policy_arns": [], + "max_session_duration": 3600, + "name": "c7n-space-update-selected-quail", + "name_prefix": "", + "path": "/", + "permissions_boundary": "", + "tags": null, + "tags_all": {}, + "unique_id": "AROAZL6XWCR2L6C7NSPCE" + } + }, + "aws_sagemaker_domain": { + "main": { + "app_network_access_type": "PublicInternetOnly", + "app_security_group_management": "", + "arn": "arn:aws:sagemaker:us-east-1:644160558196:domain/d-000000000001", + "auth_mode": "IAM", + "default_space_settings": [ + { + "custom_file_system_config": [], + "custom_posix_user_config": [], + "execution_role": "arn:aws:iam::644160558196:role/c7n-space-update-selected-quail", + "jupyter_lab_app_settings": [], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [], + "security_groups": null, + "space_storage_settings": [] + } + ], + "default_user_settings": [ + { + "auto_mount_home_efs": "Enabled", + "canvas_app_settings": [], + "code_editor_app_settings": [], + "custom_file_system_config": [], + "custom_posix_user_config": [], + "default_landing_uri": "studio::", + "execution_role": "arn:aws:iam::644160558196:role/c7n-space-update-selected-quail", + "jupyter_lab_app_settings": [], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [], + "r_session_app_settings": [], + "r_studio_server_pro_app_settings": [], + "security_groups": null, + "sharing_settings": [], + "space_storage_settings": [], + "studio_web_portal": "ENABLED", + "studio_web_portal_settings": [ + { + "hidden_app_types": null, + "hidden_instance_types": null, + "hidden_ml_tools": null + } + ], + "tensor_board_app_settings": [] + } + ], + "domain_name": "c7n-space-update-selected-quail", + "domain_settings": [], + "home_efs_file_system_id": "fs-00000000000000001", + "id": "d-000000000001", + "kms_key_id": "", + "region": "us-east-1", + "retention_policy": [ + { + "home_efs_file_system": "Delete" + } + ], + "security_group_id_for_domain_boundary": "", + "single_sign_on_application_arn": "", + "single_sign_on_managed_application_instance_id": "", + "subnet_ids": [ + "subnet-00000000000000001" + ], + "tag_propagation": "DISABLED", + "tags": null, + "tags_all": {}, + "url": "https://d-000000000001.studio.us-east-1.sagemaker.aws", + "vpc_id": "vpc-00000000000000001" + } + }, + "aws_sagemaker_space": { + "classic": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "domain_id": "d-000000000001", + "home_efs_file_system_uid": "200010", + "id": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-classic-selected-quail", + "ownership_settings": [], + "region": "us-east-1", + "space_display_name": "", + "space_name": "c7n-classic-selected-quail", + "space_settings": [ + { + "app_type": "", + "code_editor_app_settings": [], + "custom_file_system": [], + "jupyter_lab_app_settings": [], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [ + { + "custom_image": [], + "default_resource_spec": [ + { + "instance_type": "ml.g4dn.xlarge", + "lifecycle_config_arn": "", + "sagemaker_image_arn": "", + "sagemaker_image_version_alias": "", + "sagemaker_image_version_arn": "" + } + ], + "lifecycle_config_arns": null + } + ], + "space_storage_settings": [] + } + ], + "space_sharing_settings": [], + "tags": null, + "tags_all": {}, + "url": "https://000000000000001.studio.us-east-1.sagemaker.aws" + }, + "codeeditor": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-codeeditor-selected-quail", + "domain_id": "d-000000000001", + "home_efs_file_system_uid": "", + "id": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-codeeditor-selected-quail", + "ownership_settings": [ + { + "owner_user_profile_name": "c7n-owner-selected-quail" + } + ], + "region": "us-east-1", + "space_display_name": "", + "space_name": "c7n-codeeditor-selected-quail", + "space_settings": [ + { + "app_type": "CodeEditor", + "code_editor_app_settings": [ + { + "app_lifecycle_management": [], + "default_resource_spec": [ + { + "instance_type": "ml.g4dn.xlarge", + "lifecycle_config_arn": "", + "sagemaker_image_arn": "", + "sagemaker_image_version_alias": "", + "sagemaker_image_version_arn": "" + } + ] + } + ], + "custom_file_system": [], + "jupyter_lab_app_settings": [], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [], + "space_storage_settings": [ + { + "ebs_storage_settings": [ + { + "ebs_volume_size_in_gb": 5 + } + ] + } + ] + } + ], + "space_sharing_settings": [ + { + "sharing_type": "Private" + } + ], + "tags": null, + "tags_all": {}, + "url": "https://000000000000002.studio.us-east-1.sagemaker.aws/codeeditor/default" + }, + "cpu": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "domain_id": "d-000000000001", + "home_efs_file_system_uid": "", + "id": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-cpu-selected-quail", + "ownership_settings": [ + { + "owner_user_profile_name": "c7n-owner-selected-quail" + } + ], + "region": "us-east-1", + "space_display_name": "", + "space_name": "c7n-cpu-selected-quail", + "space_settings": [ + { + "app_type": "JupyterLab", + "code_editor_app_settings": [], + "custom_file_system": [], + "jupyter_lab_app_settings": [ + { + "app_lifecycle_management": [], + "code_repository": [], + "default_resource_spec": [ + { + "instance_type": "ml.t3.large", + "lifecycle_config_arn": "", + "sagemaker_image_arn": "", + "sagemaker_image_version_alias": "", + "sagemaker_image_version_arn": "" + } + ] + } + ], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [], + "space_storage_settings": [ + { + "ebs_storage_settings": [ + { + "ebs_volume_size_in_gb": 5 + } + ] + } + ] + } + ], + "space_sharing_settings": [ + { + "sharing_type": "Private" + } + ], + "tags": null, + "tags_all": {}, + "url": "https://000000000000003.studio.us-east-1.sagemaker.aws/jupyterlab/default" + }, + "jupyterlab": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "domain_id": "d-000000000001", + "home_efs_file_system_uid": "", + "id": "arn:aws:sagemaker:us-east-1:644160558196:space/d-000000000001/c7n-jupyterlab-selected-quail", + "ownership_settings": [ + { + "owner_user_profile_name": "c7n-owner-selected-quail" + } + ], + "region": "us-east-1", + "space_display_name": "", + "space_name": "c7n-jupyterlab-selected-quail", + "space_settings": [ + { + "app_type": "JupyterLab", + "code_editor_app_settings": [ + { + "app_lifecycle_management": [], + "default_resource_spec": [ + { + "instance_type": "ml.g4dn.xlarge", + "lifecycle_config_arn": "", + "sagemaker_image_arn": "", + "sagemaker_image_version_alias": "", + "sagemaker_image_version_arn": "" + } + ] + } + ], + "custom_file_system": [], + "jupyter_lab_app_settings": [ + { + "app_lifecycle_management": [], + "code_repository": [], + "default_resource_spec": [ + { + "instance_type": "ml.g4dn.xlarge", + "lifecycle_config_arn": "", + "sagemaker_image_arn": "", + "sagemaker_image_version_alias": "", + "sagemaker_image_version_arn": "" + } + ] + } + ], + "jupyter_server_app_settings": [], + "kernel_gateway_app_settings": [], + "space_storage_settings": [ + { + "ebs_storage_settings": [ + { + "ebs_volume_size_in_gb": 5 + } + ] + } + ] + } + ], + "space_sharing_settings": [ + { + "sharing_type": "Private" + } + ], + "tags": null, + "tags_all": {}, + "url": "https://000000000000004.studio.us-east-1.sagemaker.aws/jupyterlab/default" + } + }, + "aws_sagemaker_user_profile": { + "owner": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:user-profile/d-000000000001/c7n-owner-selected-quail", + "domain_id": "d-000000000001", + "home_efs_file_system_uid": "200005", + "id": "d-000000000001/c7n-owner-selected-quail", + "region": "us-east-1", + "single_sign_on_user_identifier": "", + "single_sign_on_user_value": "", + "tags": null, + "tags_all": {}, + "user_profile_name": "c7n-owner-selected-quail", + "user_settings": [] + } + }, + "random_pet": { + "space": { + "id": "selected-quail", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tests/test_sagemaker.py b/tests/test_sagemaker.py index e9cacf5a2f3..3525413d3a9 100644 --- a/tests/test_sagemaker.py +++ b/tests/test_sagemaker.py @@ -6,13 +6,14 @@ import pytest from pytest_terraform import terraform +from .apicallcaptor import ApiCallCaptor from .common import BaseTest from c7n.filters.metrics import MetricsFilter from c7n.resources.sagemaker import ( SAGEMAKER_METRICS, SagemakerEndpoint, SagemakerJobQueryParser, CompilationJobQueryParser) -from c7n.exceptions import PolicyValidationError +from c7n.exceptions import PolicyExecutionError, PolicyValidationError import botocore.exceptions as b_exc @@ -1584,6 +1585,178 @@ def test_sagemaker_space(test, sagemaker_studio): 'aws_sagemaker_space.untagged.space_name'] +def space_settings(factory, fixture, space): + return factory().client('sagemaker').describe_space( + DomainId=fixture['aws_sagemaker_domain.main.id'], + SpaceName=fixture[f'aws_sagemaker_space.{space}.space_name'], + ).get('SpaceSettings', {}) + + +def sent_updates(captor): + # Flight data replays the same responses whatever is sent, so the tests + # check the UpdateSpace requests themselves. + return [c['params'] for c in captor.calls if c.get('operation') == 'UpdateSpace'] + + +@pytest.mark.audited +@terraform('sagemaker_space_update', scope='module') +def test_sagemaker_space_update(test, sagemaker_space_update): + # GPU defaults are reset to CPU. Each space only gets the app settings it + # already has, and the Code Editor space, which has neither, is skipped. + factory = test.replay_flight_data('test_sagemaker_space_update') + fixture = sagemaker_space_update + cpu = {'DefaultResourceSpec': {'InstanceType': 'ml.t3.medium'}} + p = test.load_policy( + { + 'name': 'sagemaker-space-update', + 'resource': 'aws.sagemaker-space', + 'filters': [ + {'DomainId': fixture['aws_sagemaker_domain.main.id']}, + {'or': [ + {'type': 'value', 'op': 'glob', 'value': 'ml.g*', + 'key': 'SpaceSettings.JupyterLabAppSettings' + '.DefaultResourceSpec.InstanceType'}, + {'type': 'value', 'op': 'glob', 'value': 'ml.g*', + 'key': 'SpaceSettings.CodeEditorAppSettings' + '.DefaultResourceSpec.InstanceType'}, + {'type': 'value', 'op': 'glob', 'value': 'ml.g*', + 'key': 'SpaceSettings.KernelGatewayAppSettings' + '.DefaultResourceSpec.InstanceType'}, + ]}, + ], + 'actions': [{ + 'type': 'update', + 'SpaceSettings': { + 'JupyterLabAppSettings': cpu, + 'KernelGatewayAppSettings': cpu, + }, + }], + }, + session_factory=factory, + ) + captor = ApiCallCaptor.start_capture() + resources = p.run() + assert {r['SpaceName'] for r in resources} == { + fixture['aws_sagemaker_space.jupyterlab.space_name'], + fixture['aws_sagemaker_space.codeeditor.space_name'], + fixture['aws_sagemaker_space.classic.space_name'], + } + assert {u['SpaceName']: u['SpaceSettings'] for u in sent_updates(captor)} == { + fixture['aws_sagemaker_space.jupyterlab.space_name']: {'JupyterLabAppSettings': cpu}, + fixture['aws_sagemaker_space.classic.space_name']: {'KernelGatewayAppSettings': cpu}, + } + + if test.recording: + # UpdateSpace returns while the space is still Updating. + time.sleep(30) + jupyterlab = space_settings(factory, fixture, 'jupyterlab') + assert jupyterlab['JupyterLabAppSettings'] == cpu + assert jupyterlab['CodeEditorAppSettings']['DefaultResourceSpec'] == { + 'InstanceType': 'ml.g4dn.xlarge'} + assert 'KernelGatewayAppSettings' not in jupyterlab + + classic = space_settings(factory, fixture, 'classic') + assert classic['KernelGatewayAppSettings'] == cpu + assert 'JupyterLabAppSettings' not in classic + + codeeditor = space_settings(factory, fixture, 'codeeditor') + assert codeeditor['CodeEditorAppSettings']['DefaultResourceSpec'] == { + 'InstanceType': 'ml.g4dn.xlarge'} + assert 'JupyterLabAppSettings' not in codeeditor + assert 'KernelGatewayAppSettings' not in codeeditor + + +@pytest.mark.audited +@terraform('sagemaker_space_update', scope='module') +def test_sagemaker_space_update_error(test, sagemaker_space_update): + # SageMaker refuses to set an AppType on the classic space, which has none. + # It's listed first, so this also checks the cpu space after it is still + # updated before the failure is raised. + factory = test.replay_flight_data('test_sagemaker_space_update_error') + fixture = sagemaker_space_update + cpu_default = {'DefaultResourceSpec': {'InstanceType': 'ml.t3.xlarge'}} + p = test.load_policy( + { + 'name': 'sagemaker-space-update-error', + 'resource': 'aws.sagemaker-space', + 'filters': [ + {'DomainId': fixture['aws_sagemaker_domain.main.id']}, + {'type': 'value', 'key': 'SpaceName', 'op': 'in', 'value': [ + fixture['aws_sagemaker_space.classic.space_name'], + fixture['aws_sagemaker_space.cpu.space_name'], + ]}, + ], + 'actions': [{ + 'type': 'update', + 'SpaceSettings': { + 'AppType': 'JupyterLab', + 'JupyterLabAppSettings': cpu_default, + }, + }], + }, + session_factory=factory, + ) + captor = ApiCallCaptor.start_capture() + with pytest.raises(PolicyExecutionError, match='1 of 2 spaces failed'): + p.run() + assert [(u['SpaceName'], u['SpaceSettings']) for u in sent_updates(captor)] == [ + (fixture['aws_sagemaker_space.classic.space_name'], + {'AppType': 'JupyterLab'}), + (fixture['aws_sagemaker_space.cpu.space_name'], + {'AppType': 'JupyterLab', 'JupyterLabAppSettings': cpu_default}), + ] + + if test.recording: + time.sleep(30) + cpu = space_settings(factory, fixture, 'cpu') + assert cpu['JupyterLabAppSettings'] == cpu_default + + +def test_sagemaker_space_update_validation(test): + with pytest.raises(PolicyValidationError, match='InstnaceType'): + test.load_policy( + { + 'name': 'sagemaker-space-update-typo', + 'resource': 'aws.sagemaker-space', + 'actions': [{ + 'type': 'update', + 'SpaceSettings': { + 'JupyterLabAppSettings': { + 'DefaultResourceSpec': {'InstnaceType': 'ml.t3.medium'}}, + }, + }], + }, + validate=True, + ) + + +def test_sagemaker_space_update_fit_to_space(test): + # Flight data replays without checking what was sent, so the recorded + # tests can't catch a space being sent app settings it doesn't have. + cpu = {'DefaultResourceSpec': {'InstanceType': 'ml.t3.medium'}} + p = test.load_policy({ + 'name': 'sagemaker-space-update-fit', + 'resource': 'aws.sagemaker-space', + 'actions': [{ + 'type': 'update', + 'SpaceSettings': { + 'JupyterLabAppSettings': cpu, + 'KernelGatewayAppSettings': cpu, + }, + }], + }) + action = p.resource_manager.actions[0] + + jupyterlab = {'SpaceSettings': {'AppType': 'JupyterLab', 'JupyterLabAppSettings': {}}} + classic = {'SpaceSettings': {'KernelGatewayAppSettings': {}}} + codeeditor = {'SpaceSettings': {'CodeEditorAppSettings': {}}} + assert action.fit_to_space(jupyterlab) == { + 'SpaceSettings': {'JupyterLabAppSettings': cpu}} + assert action.fit_to_space(classic) == { + 'SpaceSettings': {'KernelGatewayAppSettings': cpu}} + assert action.fit_to_space(codeeditor) == {} + + @pytest.mark.audited @terraform('sagemaker_studio', scope='module') def test_sagemaker_app(test, sagemaker_studio): From 1f5bd1d7926542cdfa578ea73f008274b857a980 Mon Sep 17 00:00:00 2001 From: Sayak Satpathi Date: Sat, 17 Oct 2026 10:36:04 +0530 Subject: [PATCH 49/65] aws - cfn - skip ROLLBACK_COMPLETE stacks in tag actions (#11215) --------- Co-authored-by: Sayak Satpathi <170233065+sayaksatpathi@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- c7n/resources/cfn.py | 5 +++-- tests/test_cfn.py | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/c7n/resources/cfn.py b/c7n/resources/cfn.py index 14d2f4f9838..92196ced3ee 100644 --- a/c7n/resources/cfn.py +++ b/c7n/resources/cfn.py @@ -17,14 +17,15 @@ log = logging.getLogger('custodian.cfn') -# Stable states that allow UpdateStack operations (including tagging) +# Stable states that allow UpdateStack operations (including tagging). +# ROLLBACK_COMPLETE is not one of them: after a failed stack creation the +# only operation CloudFormation allows is a delete. CFN_UPDATEABLE_STATES = frozenset([ 'CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_COMPLETE', 'IMPORT_COMPLETE', 'IMPORT_ROLLBACK_COMPLETE', - 'ROLLBACK_COMPLETE', ]) diff --git a/tests/test_cfn.py b/tests/test_cfn.py index a8e9d82eb81..780076ffcfe 100644 --- a/tests/test_cfn.py +++ b/tests/test_cfn.py @@ -338,7 +338,6 @@ def test_cfn_stack_updatable(self): self.assertTrue(cfn._cfn_stack_updatable({'StackStatus': 'UPDATE_ROLLBACK_COMPLETE'})) self.assertTrue(cfn._cfn_stack_updatable({'StackStatus': 'IMPORT_COMPLETE'})) self.assertTrue(cfn._cfn_stack_updatable({'StackStatus': 'IMPORT_ROLLBACK_COMPLETE'})) - self.assertTrue(cfn._cfn_stack_updatable({'StackStatus': 'ROLLBACK_COMPLETE'})) # Non-updateable states - should return False self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'CREATE_IN_PROGRESS'})) @@ -349,6 +348,12 @@ def test_cfn_stack_updatable(self): self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'CREATE_FAILED'})) self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'ROLLBACK_IN_PROGRESS'})) self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'UPDATE_ROLLBACK_IN_PROGRESS'})) + # A stack whose creation rolled back can only be deleted + self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'ROLLBACK_COMPLETE'})) + self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'ROLLBACK_FAILED'})) + self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'UPDATE_ROLLBACK_FAILED'})) + self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'IMPORT_ROLLBACK_FAILED'})) + self.assertFalse(cfn._cfn_stack_updatable({'StackStatus': 'REVIEW_IN_PROGRESS'})) # Edge cases self.assertFalse(cfn._cfn_stack_updatable({})) # Missing StackStatus @@ -361,6 +366,8 @@ def test_cfn_tag_skip_non_updateable(self): {'StackName': 'stack-ok', 'StackStatus': 'CREATE_COMPLETE', 'Tags': []}, {'StackName': 'stack-in-progress', 'StackStatus': 'UPDATE_IN_PROGRESS', 'Tags': []}, {'StackName': 'stack-failed', 'StackStatus': 'DELETE_FAILED', 'Tags': []}, + {'StackName': 'stack-rolled-back', 'StackStatus': 'ROLLBACK_COMPLETE', 'Tags': []}, + {'StackName': 'stack-after', 'StackStatus': 'UPDATE_COMPLETE', 'Tags': []}, ] action = MagicMock(spec=CloudFormationAddTag) @@ -371,17 +378,16 @@ def test_cfn_tag_skip_non_updateable(self): CloudFormationAddTag.process_resource_set( action, MagicMock(), stacks, tags) - # Should only tag the one stack in updateable state - self.assertEqual(mock_tag_stack.call_count, 1) - # Verify the stack that was tagged (positional args: client, stack, add=tags) - call_args = mock_tag_stack.call_args - tagged_stack = call_args[0][1] # Second positional arg is the stack - self.assertEqual(tagged_stack['StackName'], 'stack-ok') + # Only stacks in an updateable state are tagged, including one + # that comes after the skipped stacks + # (positional args: client, stack, add=tags) + tagged = [c[0][1]['StackName'] for c in mock_tag_stack.call_args_list] + self.assertEqual(tagged, ['stack-ok', 'stack-after']) # Verify warning was logged for skipped stacks action.log.warning.assert_called_once() warning_args = action.log.warning.call_args[0] - self.assertIn('2', str(warning_args)) # 2 stacks skipped + self.assertIn('3', str(warning_args)) # 3 stacks skipped def test_cfn_remove_tag_skip_non_updateable(self): """Test that removing tags skips stacks in non-updateable states.""" From 2af058a943bdee719137342d045626e7137e60c8 Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Mon, 19 Oct 2026 13:26:39 -0400 Subject: [PATCH 50/65] gcp - app-engine-service-version - fetch full view and add audit mode support (#11173) --- tools/c7n_gcp/c7n_gcp/resources/appengine.py | 24 +- .../app-engine-version-create-first.json | 72 +++++ .../app-engine-version-create-last.json | 46 +++ ...es-default-versions-c7n-always-fd72_1.json | 70 +++++ ...es-default-versions-c7n-always-fd72_2.json | 70 +++++ ...ps-cloud-custodian-services-default_1.json | 26 ++ ...ps-cloud-custodian-services-default_2.json | 26 ++ ...gleapis.com-v1-apps-cloud-custodian_1.json | 35 +++ ...gleapis.com-v1-apps-cloud-custodian_2.json | 35 +++ ...custodian-services-default-versions_1.json | 179 +++++++++++ ...om-v1-apps-cloud-custodian-services_1.json | 30 ++ ...gleapis.com-v1-apps-cloud-custodian_1.json | 35 +++ .../app_engine_service_version/README.md | 86 +++++ .../app_engine_service_version/app/app.js | 3 + .../app/package.json | 6 + .../app_engine_service_version/main.tf | 75 +++++ .../tf_resources.json | 297 ++++++++++++++++++ tools/c7n_gcp/tests/test_appengine.py | 98 +++++- 18 files changed, 1211 insertions(+), 2 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/events/app-engine-version-create-first.json create mode 100644 tools/c7n_gcp/tests/data/events/app-engine-version-create-last.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json create mode 100644 tools/c7n_gcp/tests/terraform/app_engine_service_version/README.md create mode 100644 tools/c7n_gcp/tests/terraform/app_engine_service_version/app/app.js create mode 100644 tools/c7n_gcp/tests/terraform/app_engine_service_version/app/package.json create mode 100644 tools/c7n_gcp/tests/terraform/app_engine_service_version/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/app_engine_service_version/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/appengine.py b/tools/c7n_gcp/c7n_gcp/resources/appengine.py index 51f222c3d2d..d2439e4f592 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/appengine.py +++ b/tools/c7n_gcp/c7n_gcp/resources/appengine.py @@ -171,17 +171,34 @@ def get(client, resource_info): 'servicesId': service_id}) +VERSION_NAME_RE = re.compile( + r'(?:.*/)?apps/(?P[^/]+)' + r'/services/(?P[^/]+)' + r'/versions/(?P[^/]+)$') + + +def parse_version_name(name): + """Split a version name into its appsId, servicesId and versionsId path params.""" + if match := VERSION_NAME_RE.match(name): + return match.groupdict() + raise ValueError(f"Couldn't parse app, service and version from {name}") + + @resources.register('app-engine-service-version') class AppEngineServiceVersion(ChildResourceManager): """GCP Resource https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions """ + def _get_parent_resource_info(self, child_instance): + ids = parse_version_name(child_instance['name']) + return {'resourceName': f"apps/{ids['appsId']}/services/{ids['servicesId']}"} + class resource_type(AppEngineChildTypeInfo): component = 'apps.services.versions' name = 'name' id = 'id' - enum_spec = ('list', 'versions[]', None) + enum_spec = ('list', 'versions[]', {'view': 'FULL'}) default_report_fields = ['name', 'instanceClass', 'runtime', 'runtimeChannel', 'vm'] urn_component = "versions" asset_type = "appengine.googleapis.com/Version" @@ -192,3 +209,8 @@ class resource_type(AppEngineChildTypeInfo): ('name', 'appsId', 'regex', r'/(.*?)/') ] } + + @staticmethod + def get(client, resource_info): + ids = parse_version_name(resource_info['resourceName']) + return client.execute_query('get', {**ids, 'view': 'FULL'}) diff --git a/tools/c7n_gcp/tests/data/events/app-engine-version-create-first.json b/tools/c7n_gcp/tests/data/events/app-engine-version-create-first.json new file mode 100644 index 00000000000..fa0b656fea8 --- /dev/null +++ b/tools/c7n_gcp/tests/data/events/app-engine-version-create-first.json @@ -0,0 +1,72 @@ +{ + "insertId": "q352tndjl20", + "logName": "projects/cloud-custodian/logs/cloudaudit.googleapis.com%2Factivity", + "operation": { + "first": true, + "id": "operation-1790883212996-65ccc7dbeae98-27695256-2dd852e5", + "producer": "appengine.googleapis.com" + }, + "protoPayload": { + "@type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": { + "oauthInfo": { + "oauthClientId": "32555940559.apps.googleusercontent.com" + }, + "principalEmail": "user@example.com" + }, + "authorizationInfo": [ + { + "granted": true, + "permission": "appengine.versions.create", + "permissionType": "ADMIN_WRITE", + "resource": "projects/cloud-custodian/locations/us-central1/applications/cloud-custodian/services/default", + "resourceAttributes": {} + } + ], + "metadata": { + "@type": "type.googleapis.com/google.appengine.v1.CreateVersionRequest", + "parent": "projects/cloud-custodian/locations/us-central1/applications/cloud-custodian/services/default", + "version": { + "entrypoint": { + "shell": "node app.js" + }, + "id": "c7n-always-fd72", + "runtime": "nodejs24" + } + }, + "methodName": "google.appengine.v1.Versions.CreateVersion", + "request": { + "@type": "type.googleapis.com/google.appengine.v1.CreateVersionRequest", + "parent": "projects/cloud-custodian/locations/us-central1/applications/cloud-custodian/services/default" + }, + "requestMetadata": { + "callerIp": "198.51.100.1", + "callerSuppliedUserAgent": "Terraform/1.11.5 (+https://www.terraform.io) Terraform-Plugin-SDK/2.40.1 terraform-provider-google/dev8,gzip(gfe)", + "destinationAttributes": {}, + "requestAttributes": { + "auth": {}, + "time": "2026-10-01T19:33:32.976233876Z" + } + }, + "resourceLocation": { + "currentLocations": [ + "us-central1" + ] + }, + "resourceName": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "serviceName": "appengine.googleapis.com", + "status": {} + }, + "receiveTimestamp": "2026-10-01T19:33:33.666377664Z", + "resource": { + "labels": { + "module_id": "default", + "project_id": "cloud-custodian", + "version_id": "c7n-always-fd72", + "zone": "" + }, + "type": "gae_app" + }, + "severity": "NOTICE", + "timestamp": "2026-10-01T19:33:32.946509587Z" +} diff --git a/tools/c7n_gcp/tests/data/events/app-engine-version-create-last.json b/tools/c7n_gcp/tests/data/events/app-engine-version-create-last.json new file mode 100644 index 00000000000..c5d1bb75e7e --- /dev/null +++ b/tools/c7n_gcp/tests/data/events/app-engine-version-create-last.json @@ -0,0 +1,46 @@ +{ + "insertId": "3m1ytqcld1", + "logName": "projects/cloud-custodian/logs/cloudaudit.googleapis.com%2Factivity", + "operation": { + "id": "operation-1790883212996-65ccc7dbeae98-27695256-2dd852e5", + "last": true, + "producer": "appengine.googleapis.com" + }, + "protoPayload": { + "@type": "type.googleapis.com/google.cloud.audit.AuditLog", + "authenticationInfo": { + "oauthInfo": { + "oauthClientId": "32555940559.apps.googleusercontent.com" + }, + "principalEmail": "user@example.com" + }, + "methodName": "google.appengine.v1.Versions.CreateVersion", + "requestMetadata": { + "destinationAttributes": {}, + "requestAttributes": {} + }, + "resourceLocation": { + "currentLocations": [ + "us-central1" + ] + }, + "resourceName": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "response": { + "@type": "type.googleapis.com/google.appengine.v1.Version" + }, + "serviceName": "appengine.googleapis.com", + "status": {} + }, + "receiveTimestamp": "2026-10-01T19:34:01.732298958Z", + "resource": { + "labels": { + "module_id": "default", + "project_id": "cloud-custodian", + "version_id": "c7n-always-fd72", + "zone": "" + }, + "type": "gae_app" + }, + "severity": "NOTICE", + "timestamp": "2026-10-01T19:34:01.707367737Z" +} diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_1.json new file mode 100644 index 00000000000..561b6f3d1e5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_1.json @@ -0,0 +1,70 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:20 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1415", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services/default/versions/c7n-always-fd72?view=FULL&alt=json" + }, + "body": { + "name": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "id": "c7n-always-fd72", + "automaticScaling": { + "standardSchedulerSettings": { + "maxInstances": 20 + } + }, + "instanceClass": "F1", + "network": {}, + "runtime": "nodejs24", + "threadsafe": true, + "env": "standard", + "servingStatus": "SERVING", + "createdBy": "user@example.com", + "createTime": "2026-10-01T19:33:54Z", + "diskUsageBytes": "2980977", + "handlers": [ + { + "urlRegex": "/.*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_ALWAYS", + "login": "LOGIN_OPTIONAL", + "authFailAction": "AUTH_FAIL_ACTION_REDIRECT", + "redirectHttpResponseCode": "REDIRECT_HTTP_RESPONSE_CODE_301" + }, + { + "urlRegex": ".*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL" + } + ], + "deployment": { + "files": { + "app.js": { + "sourceUrl": "app.js", + "sha1Sum": "cec0aed61e8d3c52cb7278c2ebfe0671a834a8c2" + }, + "package.json": { + "sourceUrl": "package.json", + "sha1Sum": "98c170b7167773167da588abffb0eaeb8b5d90e2" + } + } + }, + "versionUrl": "https://c7n-always-fd72-dot-cloud-custodian.uc.r.appspot.com", + "runtimeChannel": "default", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_2.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_2.json new file mode 100644 index 00000000000..561b6f3d1e5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions-c7n-always-fd72_2.json @@ -0,0 +1,70 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:20 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1415", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services/default/versions/c7n-always-fd72?view=FULL&alt=json" + }, + "body": { + "name": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "id": "c7n-always-fd72", + "automaticScaling": { + "standardSchedulerSettings": { + "maxInstances": 20 + } + }, + "instanceClass": "F1", + "network": {}, + "runtime": "nodejs24", + "threadsafe": true, + "env": "standard", + "servingStatus": "SERVING", + "createdBy": "user@example.com", + "createTime": "2026-10-01T19:33:54Z", + "diskUsageBytes": "2980977", + "handlers": [ + { + "urlRegex": "/.*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_ALWAYS", + "login": "LOGIN_OPTIONAL", + "authFailAction": "AUTH_FAIL_ACTION_REDIRECT", + "redirectHttpResponseCode": "REDIRECT_HTTP_RESPONSE_CODE_301" + }, + { + "urlRegex": ".*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL" + } + ], + "deployment": { + "files": { + "app.js": { + "sourceUrl": "app.js", + "sha1Sum": "cec0aed61e8d3c52cb7278c2ebfe0671a834a8c2" + }, + "package.json": { + "sourceUrl": "package.json", + "sha1Sum": "98c170b7167773167da588abffb0eaeb8b5d90e2" + } + } + }, + "versionUrl": "https://c7n-always-fd72-dot-cloud-custodian.uc.r.appspot.com", + "runtimeChannel": "default", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_1.json new file mode 100644 index 00000000000..08fff8892a7 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_1.json @@ -0,0 +1,26 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:20 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "148", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services/default?alt=json" + }, + "body": { + "name": "apps/cloud-custodian/services/default", + "id": "default", + "split": { + "allocations": { + "c7n-always-c5a1": 1 + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_2.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_2.json new file mode 100644 index 00000000000..2c56f9a66d5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default_2.json @@ -0,0 +1,26 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:21 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "148", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services/default?alt=json" + }, + "body": { + "name": "apps/cloud-custodian/services/default", + "id": "default", + "split": { + "allocations": { + "c7n-always-c5a1": 1 + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json new file mode 100644 index 00000000000..4a762bc37eb --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:20 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "599", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian?alt=json" + }, + "body": { + "name": "apps/cloud-custodian", + "id": "cloud-custodian", + "authDomain": "gmail.com", + "locationId": "us-central", + "codeBucket": "staging.cloud-custodian.appspot.com", + "servingStatus": "SERVING", + "defaultHostname": "cloud-custodian.uc.r.appspot.com", + "defaultBucket": "cloud-custodian.appspot.com", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com", + "gcrDomain": "us.gcr.io", + "databaseType": "CLOUD_DATASTORE_COMPATIBILITY", + "featureSettings": { + "splitHealthChecks": true, + "useContainerOptimizedOs": true + }, + "sslPolicy": "MODERN" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_2.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_2.json new file mode 100644 index 00000000000..599c1be6dd8 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_audit/get-appengine.googleapis.com-v1-apps-cloud-custodian_2.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:21 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "599", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian?alt=json" + }, + "body": { + "name": "apps/cloud-custodian", + "id": "cloud-custodian", + "authDomain": "gmail.com", + "locationId": "us-central", + "codeBucket": "staging.cloud-custodian.appspot.com", + "servingStatus": "SERVING", + "defaultHostname": "cloud-custodian.uc.r.appspot.com", + "defaultBucket": "cloud-custodian.appspot.com", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com", + "gcrDomain": "us.gcr.io", + "databaseType": "CLOUD_DATASTORE_COMPATIBILITY", + "featureSettings": { + "splitHealthChecks": true, + "useContainerOptimizedOs": true + }, + "sslPolicy": "MODERN" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions_1.json new file mode 100644 index 00000000000..4d0fbadf97c --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services-default-versions_1.json @@ -0,0 +1,179 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:06 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "4842", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services/default/versions?view=FULL&alt=json" + }, + "body": { + "versions": [ + { + "name": "apps/cloud-custodian/services/default/versions/c7n-always-c5a1", + "id": "c7n-always-c5a1", + "automaticScaling": { + "standardSchedulerSettings": { + "maxInstances": 20 + } + }, + "instanceClass": "F1", + "network": {}, + "runtime": "nodejs24", + "threadsafe": true, + "env": "standard", + "servingStatus": "SERVING", + "createdBy": "user@example.com", + "createTime": "2026-10-01T17:53:38Z", + "diskUsageBytes": "2980977", + "handlers": [ + { + "urlRegex": "/.*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_ALWAYS", + "login": "LOGIN_OPTIONAL", + "authFailAction": "AUTH_FAIL_ACTION_REDIRECT", + "redirectHttpResponseCode": "REDIRECT_HTTP_RESPONSE_CODE_301" + }, + { + "urlRegex": ".*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL" + } + ], + "deployment": { + "files": { + "app.js": { + "sourceUrl": "app.js", + "sha1Sum": "cec0aed61e8d3c52cb7278c2ebfe0671a834a8c2" + }, + "package.json": { + "sourceUrl": "package.json", + "sha1Sum": "98c170b7167773167da588abffb0eaeb8b5d90e2" + } + } + }, + "versionUrl": "https://c7n-always-c5a1-dot-cloud-custodian.uc.r.appspot.com", + "runtimeChannel": "default", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com" + }, + { + "name": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "id": "c7n-always-fd72", + "automaticScaling": { + "standardSchedulerSettings": { + "maxInstances": 20 + } + }, + "instanceClass": "F1", + "network": {}, + "runtime": "nodejs24", + "threadsafe": true, + "env": "standard", + "servingStatus": "SERVING", + "createdBy": "user@example.com", + "createTime": "2026-10-01T19:33:54Z", + "diskUsageBytes": "2980977", + "handlers": [ + { + "urlRegex": "/.*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_ALWAYS", + "login": "LOGIN_OPTIONAL", + "authFailAction": "AUTH_FAIL_ACTION_REDIRECT", + "redirectHttpResponseCode": "REDIRECT_HTTP_RESPONSE_CODE_301" + }, + { + "urlRegex": ".*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL" + } + ], + "deployment": { + "files": { + "app.js": { + "sourceUrl": "app.js", + "sha1Sum": "cec0aed61e8d3c52cb7278c2ebfe0671a834a8c2" + }, + "package.json": { + "sourceUrl": "package.json", + "sha1Sum": "98c170b7167773167da588abffb0eaeb8b5d90e2" + } + } + }, + "versionUrl": "https://c7n-always-fd72-dot-cloud-custodian.uc.r.appspot.com", + "runtimeChannel": "default", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com" + }, + { + "name": "apps/cloud-custodian/services/default/versions/c7n-optional-fd72", + "id": "c7n-optional-fd72", + "automaticScaling": { + "standardSchedulerSettings": { + "maxInstances": 20 + } + }, + "instanceClass": "F1", + "network": {}, + "runtime": "nodejs24", + "threadsafe": true, + "env": "standard", + "servingStatus": "SERVING", + "createdBy": "user@example.com", + "createTime": "2026-10-01T19:33:25Z", + "diskUsageBytes": "2980977", + "handlers": [ + { + "urlRegex": "/.*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL", + "authFailAction": "AUTH_FAIL_ACTION_REDIRECT" + }, + { + "urlRegex": ".*", + "script": { + "scriptPath": "auto" + }, + "securityLevel": "SECURE_OPTIONAL", + "login": "LOGIN_OPTIONAL" + } + ], + "deployment": { + "files": { + "app.js": { + "sourceUrl": "app.js", + "sha1Sum": "cec0aed61e8d3c52cb7278c2ebfe0671a834a8c2" + }, + "package.json": { + "sourceUrl": "package.json", + "sha1Sum": "98c170b7167773167da588abffb0eaeb8b5d90e2" + } + } + }, + "versionUrl": "https://c7n-optional-fd72-dot-cloud-custodian.uc.r.appspot.com", + "runtimeChannel": "default", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com" + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services_1.json new file mode 100644 index 00000000000..783898f94cc --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian-services_1.json @@ -0,0 +1,30 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:05 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "208", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian/services?alt=json" + }, + "body": { + "services": [ + { + "name": "apps/cloud-custodian/services/default", + "id": "default", + "split": { + "allocations": { + "c7n-always-c5a1": 1 + } + } + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json new file mode 100644 index 00000000000..08549a5daf3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/app_engine_service_version_full_view/get-appengine.googleapis.com-v1-apps-cloud-custodian_1.json @@ -0,0 +1,35 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Thu, 01 Oct 2026 19:34:05 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "599", + "-content-encoding": "gzip", + "content-location": "https://appengine.googleapis.com/v1/apps/cloud-custodian?alt=json" + }, + "body": { + "name": "apps/cloud-custodian", + "id": "cloud-custodian", + "authDomain": "gmail.com", + "locationId": "us-central", + "codeBucket": "staging.cloud-custodian.appspot.com", + "servingStatus": "SERVING", + "defaultHostname": "cloud-custodian.uc.r.appspot.com", + "defaultBucket": "cloud-custodian.appspot.com", + "serviceAccount": "cloud-custodian@appspot.gserviceaccount.com", + "gcrDomain": "us.gcr.io", + "databaseType": "CLOUD_DATASTORE_COMPATIBILITY", + "featureSettings": { + "splitHealthChecks": true, + "useContainerOptimizedOs": true + }, + "sslPolicy": "MODERN" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/app_engine_service_version/README.md b/tools/c7n_gcp/tests/terraform/app_engine_service_version/README.md new file mode 100644 index 00000000000..d13b2e3b0d0 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/app_engine_service_version/README.md @@ -0,0 +1,86 @@ +# App Engine Service Version Recording + +This fixture deploys two App Engine standard versions to the `default` service: +one whose `/.*` handler is `SECURE_ALWAYS` and one whose handler is +`SECURE_OPTIONAL`. + +## Prerequisites + +- **An App Engine application.** Applications can't be deleted and their region + can't change, so a fixture that created one could only ever apply once. + + ```bash + gcloud app create --region=us-central + ``` + +- **A version in `default` that outlives the fixture.** App Engine won't delete + the last version of a service. In a new app, the first recording's teardown + fails with "Cannot delete the final version of a service", leaving one `c7n-*` + version and the `c7n-app-engine-*` bucket. The recordings are still good. Keep + the version, since it's the one later recordings need, and delete the bucket: + + ```bash + gcloud storage rm --recursive gs:// + ``` + + Later recordings tear down cleanly. + +- **APIs enabled:** + + ```bash + gcloud services enable appengine.googleapis.com cloudbuild.googleapis.com \ + storage.googleapis.com artifactregistry.googleapis.com + ``` + +- **Application default credentials** for the project: + + ```bash + gcloud auth application-default login + ``` + +## Recording + +Delete the old audit events first. The recorder won't overwrite an event file; +it writes `.json-1` beside it, and the test keeps reading the old one: + +```bash +rm tools/c7n_gcp/tests/data/events/app-engine-version-create-*.json +``` + +Switch both `test_app_engine_service_version_*` tests in `test_appengine.py` to +`replay=False` and `record_flight_data`, then run: + +```bash +C7N_FUNCTIONAL=yes GOOGLE_CLOUD_PROJECT= \ + uv run pytest -s -p no:env --tf-debug \ + tools/c7n_gcp/tests/test_appengine.py -k app_engine_service_version +``` + +`C7N_FUNCTIONAL=yes` makes `event_data` put the live project back into the +recorded audit events, so the audit test fetches a version that exists. + +The recorder only rewrites `projects//`, and App Engine paths are +`apps//`. Before committing: + +- rename the flight files and replace the project id with `cloud-custodian`, in + the flight data and in `tf_resources.json` +- replace `createdBy` with `user@example.com`, and the bucket's `project_number` + in `tf_resources.json` with `123456789012` + +Then remove `replay=False` and switch back to `replay_flight_data` in both +tests, and confirm they pass in replay: + +```bash +C7N_FUNCTIONAL=no uv run pytest \ + tools/c7n_gcp/tests/test_appengine.py -k app_engine_service_version +``` + +## Cleanup check + +```bash +gcloud app versions list --project +gcloud storage buckets list --project --filter='name~c7n-app-engine' +``` + +Only the long-lived `default` version should remain, and no `c7n-app-engine-*` +bucket. diff --git a/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/app.js b/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/app.js new file mode 100644 index 00000000000..3600ed06057 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/app.js @@ -0,0 +1,3 @@ +require('http') + .createServer((req, res) => res.end('ok')) + .listen(process.env.PORT || 8080); diff --git a/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/package.json b/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/package.json new file mode 100644 index 00000000000..627cb4ba24f --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/app_engine_service_version/app/package.json @@ -0,0 +1,6 @@ +{ + "name": "c7n-app-engine-test", + "version": "1.0.0", + "private": true, + "main": "app.js" +} diff --git a/tools/c7n_gcp/tests/terraform/app_engine_service_version/main.tf b/tools/c7n_gcp/tests/terraform/app_engine_service_version/main.tf new file mode 100644 index 00000000000..71773603416 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/app_engine_service_version/main.tf @@ -0,0 +1,75 @@ +resource "random_id" "suffix" { + byte_length = 2 +} + +# Needs an existing App Engine app in the project; see README.md. + +data "archive_file" "app_source" { + type = "zip" + source_dir = "${path.module}/app" + output_path = "${path.module}/.terraform/app.zip" +} + +resource "google_storage_bucket" "source" { + name = "c7n-app-engine-${terraform.workspace}-${random_id.suffix.hex}" + location = "US" + force_destroy = true + uniform_bucket_level_access = true +} + +resource "google_storage_bucket_object" "source" { + name = "app-${data.archive_file.app_source.output_md5}.zip" + bucket = google_storage_bucket.source.name + source = data.archive_file.app_source.output_path +} + +resource "google_app_engine_standard_app_version" "secure_always" { + service = "default" + version_id = "c7n-always-${random_id.suffix.hex}" + runtime = "nodejs24" + + entrypoint { + shell = "node app.js" + } + + deployment { + zip { + source_url = "https://storage.googleapis.com/${google_storage_bucket.source.name}/${google_storage_bucket_object.source.name}" + } + } + + handlers { + url_regex = "/.*" + security_level = "SECURE_ALWAYS" + redirect_http_response_code = "REDIRECT_HTTP_RESPONSE_CODE_301" + + script { + script_path = "auto" + } + } +} + +resource "google_app_engine_standard_app_version" "secure_optional" { + service = "default" + version_id = "c7n-optional-${random_id.suffix.hex}" + runtime = "nodejs24" + + entrypoint { + shell = "node app.js" + } + + deployment { + zip { + source_url = "https://storage.googleapis.com/${google_storage_bucket.source.name}/${google_storage_bucket_object.source.name}" + } + } + + handlers { + url_regex = "/.*" + security_level = "SECURE_OPTIONAL" + + script { + script_path = "auto" + } + } +} diff --git a/tools/c7n_gcp/tests/terraform/app_engine_service_version/tf_resources.json b/tools/c7n_gcp/tests/terraform/app_engine_service_version/tf_resources.json new file mode 100644 index 00000000000..f21575ddcee --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/app_engine_service_version/tf_resources.json @@ -0,0 +1,297 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "archive_file": { + "app_source": { + "exclude_symlink_directories": null, + "excludes": null, + "id": "c981baa1a06a699682fa5ab144b1011fbad11663", + "output_base64sha256": "a4xZl8g8wdhTVh8CfoTYiU74w2wOjpPLHyCAFY/xFGg=", + "output_base64sha512": "FqGgnGyHrkJjXl4w1ULrfnVL2XBXr4faRg7HC/yr39jqAsLGTrrjrEmBDTK24zzFJ//VmD75i7+nasbmWglHrQ==", + "output_file_mode": null, + "output_md5": "ac8fd586b5c601edb997fc3e17f1136f", + "output_path": "./.terraform/app.zip", + "output_sha": "c981baa1a06a699682fa5ab144b1011fbad11663", + "output_sha256": "6b8c5997c83cc1d853561f027e84d8894ef8c36c0e8e93cb1f2080158ff11468", + "output_sha512": "16a1a09c6c87ae42635e5e30d542eb7e754bd97057af87da460ec70bfcabdfd8ea02c2c64ebae3ac49810d32b6e33cc527ffd5983ef98bbfa76ac6e65a0947ad", + "output_size": 426, + "source": [], + "source_content": null, + "source_content_filename": null, + "source_dir": "./app", + "source_file": null, + "type": "zip" + } + }, + "google_app_engine_standard_app_version": { + "secure_always": { + "app_engine_apis": false, + "app_engine_bundled_services": null, + "automatic_scaling": [ + { + "max_concurrent_requests": 0, + "max_idle_instances": 0, + "max_pending_latency": "", + "min_idle_instances": 0, + "min_pending_latency": "", + "standard_scheduler_settings": [ + { + "max_instances": 20, + "min_instances": 0, + "target_cpu_utilization": 0, + "target_throughput_utilization": 0 + } + ] + } + ], + "basic_scaling": [], + "delete_service_on_destroy": false, + "deletion_policy": "DELETE", + "deployment": [ + { + "files": [], + "zip": [ + { + "files_count": 0, + "source_url": "https://storage.googleapis.com/c7n-app-engine-default-fd72/app-ac8fd586b5c601edb997fc3e17f1136f.zip" + } + ] + } + ], + "entrypoint": [ + { + "shell": "node app.js" + } + ], + "env_variables": null, + "handlers": [ + { + "auth_fail_action": "AUTH_FAIL_ACTION_REDIRECT", + "login": "LOGIN_OPTIONAL", + "redirect_http_response_code": "REDIRECT_HTTP_RESPONSE_CODE_301", + "script": [ + { + "script_path": "auto" + } + ], + "security_level": "SECURE_ALWAYS", + "static_files": [], + "url_regex": "/.*" + }, + { + "auth_fail_action": "", + "login": "LOGIN_OPTIONAL", + "redirect_http_response_code": "", + "script": [ + { + "script_path": "auto" + } + ], + "security_level": "SECURE_OPTIONAL", + "static_files": [], + "url_regex": ".*" + } + ], + "id": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "inbound_services": null, + "instance_class": "F1", + "libraries": [], + "manual_scaling": [], + "name": "apps/cloud-custodian/services/default/versions/c7n-always-fd72", + "noop_on_destroy": false, + "project": "cloud-custodian", + "runtime": "nodejs24", + "runtime_api_version": "", + "service": "default", + "service_account": "cloud-custodian@appspot.gserviceaccount.com", + "threadsafe": null, + "timeouts": null, + "version_id": "c7n-always-fd72", + "vpc_access_connector": [] + }, + "secure_optional": { + "app_engine_apis": false, + "app_engine_bundled_services": null, + "automatic_scaling": [ + { + "max_concurrent_requests": 0, + "max_idle_instances": 0, + "max_pending_latency": "", + "min_idle_instances": 0, + "min_pending_latency": "", + "standard_scheduler_settings": [ + { + "max_instances": 20, + "min_instances": 0, + "target_cpu_utilization": 0, + "target_throughput_utilization": 0 + } + ] + } + ], + "basic_scaling": [], + "delete_service_on_destroy": false, + "deletion_policy": "DELETE", + "deployment": [ + { + "files": [], + "zip": [ + { + "files_count": 0, + "source_url": "https://storage.googleapis.com/c7n-app-engine-default-fd72/app-ac8fd586b5c601edb997fc3e17f1136f.zip" + } + ] + } + ], + "entrypoint": [ + { + "shell": "node app.js" + } + ], + "env_variables": null, + "handlers": [ + { + "auth_fail_action": "AUTH_FAIL_ACTION_REDIRECT", + "login": "LOGIN_OPTIONAL", + "redirect_http_response_code": "", + "script": [ + { + "script_path": "auto" + } + ], + "security_level": "SECURE_OPTIONAL", + "static_files": [], + "url_regex": "/.*" + }, + { + "auth_fail_action": "", + "login": "LOGIN_OPTIONAL", + "redirect_http_response_code": "", + "script": [ + { + "script_path": "auto" + } + ], + "security_level": "SECURE_OPTIONAL", + "static_files": [], + "url_regex": ".*" + } + ], + "id": "apps/cloud-custodian/services/default/versions/c7n-optional-fd72", + "inbound_services": null, + "instance_class": "F1", + "libraries": [], + "manual_scaling": [], + "name": "apps/cloud-custodian/services/default/versions/c7n-optional-fd72", + "noop_on_destroy": false, + "project": "cloud-custodian", + "runtime": "nodejs24", + "runtime_api_version": "", + "service": "default", + "service_account": "cloud-custodian@appspot.gserviceaccount.com", + "threadsafe": null, + "timeouts": null, + "version_id": "c7n-optional-fd72", + "vpc_access_connector": [] + } + }, + "google_storage_bucket": { + "source": { + "autoclass": [], + "cors": [], + "custom_placement_config": [], + "default_event_based_hold": false, + "deletion_policy": "DELETE", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "enable_object_retention": false, + "encryption": [], + "force_destroy": true, + "hierarchical_namespace": [ + { + "enabled": false + } + ], + "id": "c7n-app-engine-default-fd72", + "ip_filter": [], + "labels": null, + "lifecycle_rule": [], + "location": "US", + "logging": [], + "name": "c7n-app-engine-default-fd72", + "project": "cloud-custodian", + "project_number": 123456789012, + "public_access_prevention": "inherited", + "requester_pays": false, + "retention_policy": [], + "rpo": "DEFAULT", + "self_link": "https://www.googleapis.com/storage/v1/b/c7n-app-engine-default-fd72", + "soft_delete_policy": [ + { + "effective_time": "2026-10-01T19:33:00.633Z", + "retention_duration_seconds": 604800 + } + ], + "storage_class": "STANDARD", + "terraform_labels": { + "goog-terraform-provisioned": "true" + }, + "time_created": "2026-10-01T19:33:00.633Z", + "timeouts": null, + "uniform_bucket_level_access": true, + "updated": "2026-10-01T19:33:00.633Z", + "url": "gs://c7n-app-engine-default-fd72", + "versioning": [], + "website": [] + } + }, + "google_storage_bucket_object": { + "source": { + "bucket": "c7n-app-engine-default-fd72", + "cache_control": "", + "content": null, + "content_disposition": "", + "content_encoding": "", + "content_language": "", + "content_type": "application/zip", + "contexts": [], + "crc32c": "dndRpw==", + "customer_encryption": [], + "deletion_policy": "DELETE", + "detect_md5hash": "rI/VhrXGAe25l/w+F/ETbw==", + "event_based_hold": false, + "force_empty_content_type": null, + "generation": 1790883181370580, + "id": "c7n-app-engine-default-fd72-app-ac8fd586b5c601edb997fc3e17f1136f.zip", + "kms_key_name": "", + "md5hash": "rI/VhrXGAe25l/w+F/ETbw==", + "md5hexhash": "ac8fd586b5c601edb997fc3e17f1136f", + "media_link": "https://storage.googleapis.com/download/storage/v1/b/c7n-app-engine-default-fd72/o/app-ac8fd586b5c601edb997fc3e17f1136f.zip?generation=1790883181370580&alt=media", + "metadata": null, + "name": "app-ac8fd586b5c601edb997fc3e17f1136f.zip", + "output_name": "app-ac8fd586b5c601edb997fc3e17f1136f.zip", + "retention": [], + "self_link": "https://www.googleapis.com/storage/v1/b/c7n-app-engine-default-fd72/o/app-ac8fd586b5c601edb997fc3e17f1136f.zip", + "source": "./.terraform/app.zip", + "source_md5hash": "", + "storage_class": "STANDARD", + "temporary_hold": false, + "timeouts": null + } + }, + "random_id": { + "suffix": { + "b64_std": "/XI=", + "b64_url": "_XI", + "byte_length": 2, + "dec": "64882", + "hex": "fd72", + "id": "_XI", + "keepers": null, + "prefix": null + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_appengine.py b/tools/c7n_gcp/tests/test_appengine.py index c375f4057a0..acf9ebb6205 100644 --- a/tools/c7n_gcp/tests/test_appengine.py +++ b/tools/c7n_gcp/tests/test_appengine.py @@ -1,7 +1,13 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 -from gcp_common import BaseTest +import functools + +import pytest +from c7n_gcp.client import ServiceClient, Session +from c7n_gcp.resources.appengine import parse_version_name +from gcp_common import BaseTest, audit_event_recorder, event_data +from pytest_terraform import terraform class AppEngineAppTest(BaseTest): @@ -292,3 +298,93 @@ def test_service_version(self): resources = policy.run() self.assertEqual(resources[0]['name'], version) self.assertEqual(resources[0][parent_annotation_key]['name'], service_name) + + +def catch_all_security_levels(version): + return [h['securityLevel'] for h in version['handlers'] if h['urlRegex'] == '/.*'] + + +def capture_query_args(test, method): + """Record the arguments of every ServiceClient ``method`` call. + + Replay matches flight files by URL path alone, so recorded handlers come back + whether or not the request asked for them. Check the request instead. + """ + captured = [] + query = getattr(ServiceClient, method) + + def record(client, verb, verb_arguments): + captured.append(verb_arguments) + return query(client, verb, verb_arguments) + + test.patch(ServiceClient, method, record) + return captured + + +@terraform('app_engine_service_version', scope='session') +def test_app_engine_service_version_full_view(test, app_engine_service_version): + always = app_engine_service_version[ + 'google_app_engine_standard_app_version.secure_always.name'] + optional = app_engine_service_version[ + 'google_app_engine_standard_app_version.secure_optional.name'] + factory = test.replay_flight_data('app_engine_service_version_full_view') + list_args = capture_query_args(test, 'execute_paged_query') + policy = test.load_policy( + {'name': 'gcp-app-engine-service-version-full-view', + 'resource': 'gcp.app-engine-service-version'}, + session_factory=factory) + + versions = {v['name']: v for v in policy.run()} + + version_lists = [args for args in list_args if 'servicesId' in args] + assert version_lists + assert all(args.get('view') == 'FULL' for args in version_lists) + assert catch_all_security_levels(versions[always]) == ['SECURE_ALWAYS'] + assert catch_all_security_levels(versions[optional]) == ['SECURE_OPTIONAL'] + + +@terraform('app_engine_service_version', scope='session') +def test_app_engine_service_version_audit(test, app_engine_service_version): + name = app_engine_service_version[ + 'google_app_engine_standard_app_version.secure_always.name'] + factory = test.replay_flight_data('app_engine_service_version_audit') + + if test.recording: + setup_session_factory = functools.partial(Session, project_id=test.project_id) + audit_event_recorder( + setup_session_factory, + 'app-engine-version-create.json', + method='CreateVersion', + resource_name=name, + start_time_skew_seconds=900, + ).record() + # Otherwise the policy reuses the cached setup session and records nothing. + test.cleanUp() + + get_args = capture_query_args(test, 'execute_query') + policy = test.load_policy( + {'name': 'gcp-app-engine-service-version-audit', + 'resource': 'gcp.app-engine-service-version', + 'mode': { + 'type': 'gcp-audit', + 'methods': ['google.appengine.v1.Versions.CreateVersion']}}, + session_factory=factory) + exec_mode = policy.get_execution_mode() + parent_annotation_key = policy.resource_manager.resource_type.get_parent_annotation_key() + service = name.rsplit('/versions/', 1)[0] + + for event_file in ('app-engine-version-create-first.json', + 'app-engine-version-create-last.json'): + [version] = exec_mode.run(event_data(event_file), None) + assert version['name'] == name, event_file + assert catch_all_security_levels(version) == ['SECURE_ALWAYS'], event_file + assert version[parent_annotation_key]['name'] == service, event_file + + version_gets = [args for args in get_args if 'versionsId' in args] + assert len(version_gets) == 2 + assert all(args.get('view') == 'FULL' for args in version_gets) + + +def test_parse_version_name_rejects_service_name(): + with pytest.raises(ValueError, match='apps/cloud-custodian/services/default'): + parse_version_name('apps/cloud-custodian/services/default') From c62b221168d9850eb44904c6a2844395ee8d6213 Mon Sep 17 00:00:00 2001 From: Muhammad Saad Anjum <160885369+Saadanjum0@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:30:28 +0500 Subject: [PATCH 51/65] aws - secrets-manager - resolve secrets by arn as well as name (#11182) --- c7n/resources/secretsmanager.py | 7 +++++++ tests/test_secretsmanager.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/c7n/resources/secretsmanager.py b/c7n/resources/secretsmanager.py index dd193fc0c6f..2cb217fe962 100644 --- a/c7n/resources/secretsmanager.py +++ b/c7n/resources/secretsmanager.py @@ -16,6 +16,13 @@ class DescribeSecret(DescribeSource): + def get_resources(self, ids, cache=True): + # secrets can be referenced by name or by arn, ie. the + # secretId of a cloudtrail event is often the full arn. + ids = set(ids) + return [r for r in self.query.filter(self.manager) + if r['Name'] in ids or r['ARN'] in ids] + def _augment_secret(self, secret, client): detail_op, param_name, param_key, _ = self.manager.resource_type.detail_spec op = getattr(client, detail_op) diff --git a/tests/test_secretsmanager.py b/tests/test_secretsmanager.py index 00a03290435..cdad7ccca73 100644 --- a/tests/test_secretsmanager.py +++ b/tests/test_secretsmanager.py @@ -277,6 +277,17 @@ def test_secrets_manager_describe(self): self.assertEqual(resources[1].get('VersionIdsToStages'), None) self.assertEqual(resources[1]['c7n:DeniedMethods'], ['describe_secret']) + def test_secrets_manager_get_resources_by_arn(self): + self.patch(SecretsManager, 'executor_factory', MainThreadExecutor) + session_factory = self.replay_flight_data("test_secrets_manager_describe") + p = self.load_policy({ + "name": "get-secret-by-arn", + "resource": "aws.secrets-manager", + }, session_factory=session_factory) + arn = "arn:aws:secretsmanager:us-east-1:644160558196:secret:c7n-test-key-XTUSpD" + resources = p.resource_manager.get_resources([arn]) + self.assertEqual([r['Name'] for r in resources], ['c7n-test-key']) + def test_secrets_manager_replica_access_denied(self): self.patch(SecretsManager, 'executor_factory', MainThreadExecutor) session_factory = self.replay_flight_data('test_secrets_manager_replica_access_denied') From bcbfb73697bf44f2a6256669e35342336746be42 Mon Sep 17 00:00:00 2001 From: Usman Tahir Qureshi <123512712+UsmanQT@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:42:38 -0400 Subject: [PATCH 52/65] aws - cloudtrail - skip shadow trails in tag actions (#11187) Co-authored-by: Usman Qureshi Co-authored-by: Claude Sonnet 5.5 Co-authored-by: AJ Kerrigan --- c7n/resources/cloudtrail.py | 37 ++++++++++++++++++++- tests/test_cloudtrail.py | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/c7n/resources/cloudtrail.py b/c7n/resources/cloudtrail.py index 2b6a0c8b338..f6ebf659752 100644 --- a/c7n/resources/cloudtrail.py +++ b/c7n/resources/cloudtrail.py @@ -8,7 +8,8 @@ from c7n.exceptions import PolicyExecutionError, PolicyValidationError from c7n.filters import ValueFilter, Filter from c7n.manager import resources -from c7n.tags import universal_augment +from c7n.tags import ( + universal_augment, UniversalTag, UniversalUntag, UniversalTagDelayedAction) from c7n.query import ConfigSource, DescribeSource, QueryResourceManager, TypeInfo from c7n.utils import local_session, type_schema @@ -84,6 +85,40 @@ def is_shadow(self, t): return False +class SkipShadowTrail: + """Mixin for CloudTrail tag actions, which can't operate on shadow trails. + + The Resource Groups Tagging API rejects a trail ARN whose region differs + from the region it is invoked in, so tagging a multi-region or + organization trail from any region other than its home region always + fails. Filter shadow trails out (as update-trail, set-logging and delete + already do) so only the modifiable origin trail is acted on. + """ + + def process(self, resources): + shadow_check = IsShadow({'state': False}, self.manager) + shadow_check.embedded = True + resources = shadow_check.process(resources) + if not resources: + return + return super().process(resources) + + +@CloudTrail.action_registry.register('tag') +class CloudTrailTag(SkipShadowTrail, UniversalTag): + pass + + +@CloudTrail.action_registry.register('remove-tag') +class CloudTrailRemoveTag(SkipShadowTrail, UniversalUntag): + pass + + +@CloudTrail.action_registry.register('mark-for-op') +class CloudTrailMarkForOp(SkipShadowTrail, UniversalTagDelayedAction): + pass + + @CloudTrail.filter_registry.register('status') class Status(ValueFilter): """Filter a cloudtrail by its status. diff --git a/tests/test_cloudtrail.py b/tests/test_cloudtrail.py index 6aa4c63d0dc..2d8c299c775 100644 --- a/tests/test_cloudtrail.py +++ b/tests/test_cloudtrail.py @@ -2,11 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 import logging import time +from unittest.mock import MagicMock import pytest from pytest_terraform import terraform from c7n.exceptions import PolicyExecutionError, PolicyValidationError + from .common import BaseTest @@ -200,6 +202,69 @@ def test_cloudtrail_delete(self): Name=resources[0]['Name']) +class CloudTrailTagShadowTest(BaseTest): + # https://github.com/cloud-custodian/cloud-custodian/issues/10976 + # The Resource Groups Tagging API rejects a trail ARN whose region differs + # from the region it's invoked in, so tag actions must skip shadow trails + # (multi-region / organization trail copies seen outside their home region). + + def _policy(self, action): + mock_factory = MagicMock() + mock_factory.region = 'us-east-1' + client = mock_factory().client('resourcegroupstaggingapi') + client.tag_resources.return_value = {} + client.untag_resources.return_value = {} + policy = self.load_policy( + {'name': 't', 'resource': 'aws.cloudtrail', 'actions': [action]}, + session_factory=mock_factory) + config = policy.resource_manager.config + + arn = 'arn:aws:cloudtrail:%s:%s:trail/%s' + self.home = { + 'Name': 'home', 'TrailARN': arn % (config.region, config.account_id, 'home'), + 'IsMultiRegionTrail': True, 'HomeRegion': config.region} + # multi-region trail whose home is elsewhere + self.shadow = { + 'Name': 'shadow', 'TrailARN': arn % ('us-west-2', config.account_id, 'shadow'), + 'IsMultiRegionTrail': True, 'HomeRegion': 'us-west-2'} + # organization trail owned by another account + self.org_shadow = { + 'Name': 'org', 'TrailARN': arn % (config.region, '999999999999', 'org'), + 'IsOrganizationTrail': True, 'HomeRegion': config.region} + return policy, client + + def test_tag_skips_shadow_trails(self): + policy, client = self._policy({'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process( + [self.home, self.shadow, self.org_shadow]) + client.tag_resources.assert_called_once_with( + ResourceARNList=[self.home['TrailARN']], Tags={'Owner': 'platform'}) + + def test_remove_tag_skips_shadow_trails(self): + policy, client = self._policy({'type': 'remove-tag', 'tags': ['Owner']}) + policy.resource_manager.actions[0].process( + [self.home, self.shadow, self.org_shadow]) + client.untag_resources.assert_called_once() + self.assertEqual( + client.untag_resources.call_args.kwargs['ResourceARNList'], + [self.home['TrailARN']]) + + def test_mark_for_op_skips_shadow_trails(self): + policy, client = self._policy({'type': 'mark-for-op', 'op': 'notify', 'days': 4}) + policy.resource_manager.actions[0].process( + [self.home, self.shadow, self.org_shadow]) + client.tag_resources.assert_called_once() + self.assertEqual( + client.tag_resources.call_args.kwargs['ResourceARNList'], + [self.home['TrailARN']]) + self.assertIn('maid_status', client.tag_resources.call_args.kwargs['Tags']) + + def test_tag_all_shadow_noop(self): + policy, client = self._policy({'type': 'tag', 'tags': {'Owner': 'platform'}}) + policy.resource_manager.actions[0].process([self.shadow, self.org_shadow]) + client.tag_resources.assert_not_called() + + ADVANCED_SELECTORS = [ { 'Name': 'Log all management events', From 9c41e7e9f1a4944c851b3e6b5f237deeb6613544 Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Mon, 19 Oct 2026 21:41:14 -0400 Subject: [PATCH 53/65] core - value - add duration value_type (#11223) Co-authored-by: AJ Kerrigan Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) --- c7n/filters/core.py | 30 +- c7n/utils.py | 40 ++ docs/source/filters.rst | 15 + tests/test_filters.py | 75 ++++ tests/test_utils.py | 30 +- ...tdownTest.test_idle_shutdown_duration.json | 342 ++++++++++++++++++ .../machine-learning-compute-instance.json | 113 ++++++ .../tests_resources/test_machine_learning.py | 46 +++ 8 files changed, 689 insertions(+), 2 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/MachineLearningWorkspaceComputeIdleShutdownTest.test_idle_shutdown_duration.json create mode 100644 tools/c7n_azure/tests_azure/templates/machine-learning-compute-instance.json diff --git a/c7n/filters/core.py b/c7n/filters/core.py index 2c343aa1e0f..c62dda7532f 100644 --- a/c7n/filters/core.py +++ b/c7n/filters/core.py @@ -30,6 +30,7 @@ type_schema, parse_cidr, parse_date, + parse_duration, jmespath_search, jmespath_compile ) @@ -111,10 +112,17 @@ def mod(x, y): 'mod': mod} +# Comparisons between two timedeltas. Other operators never match a +# duration, and mod would divide by zero on a PT0S value. +DURATION_OPERATORS = { + 'eq', 'equal', 'ne', 'not-equal', 'gt', 'greater-than', 'ge', 'gte', + 'le', 'lte', 'lt', 'less-than'} + + VALUE_TYPES = [ 'age', 'integer', 'expiration', 'normalize', 'size', 'cidr', 'cidr_size', 'swap', 'resource_count', 'expr', - 'unique_size', 'date', 'version', 'float'] + 'unique_size', 'date', 'version', 'float', 'duration'] class FilterRegistry(PluginRegistry): @@ -655,6 +663,21 @@ def validate(self): raise PolicyValidationError( "value_type: date with invalid date value:%s", self.data.get('value', '')) + elif self.data.get('value_type') == 'duration': + if 'op' in self.data and self.data['op'] not in DURATION_OPERATORS: + raise PolicyValidationError( + "value_type: duration can't be used with op: %s" % self.data['op']) + if ('value' not in self.data or + 'value_from' in self.data or + 'value_path' in self.data): + raise PolicyValidationError( + "value_type: duration needs a literal value; " + "value_from and value_path aren't supported") + if parse_duration(self.data['value']) is None: + raise PolicyValidationError( + "value_type: duration needs an ISO 8601 duration in weeks, days, " + "hours, minutes or seconds, such as PT1H (years and months aren't " + "supported), got: %s" % self.data['value']) if 'key' not in self.data and 'key' in self.required_keys: raise PolicyValidationError( "Missing 'key' in value filter %s" % self.data) @@ -758,6 +781,9 @@ def match(self, i): # value type conversion if self.vtype is not None: v, r = self.process_value_type(self.v, r, i) + # a resource value that isn't a duration never matches, even for not-equal + if self.vtype == 'duration' and r is None: + return False else: v = self.v @@ -813,6 +839,8 @@ def process_value_type(self, sentinel, value, resource): return value, sentinel elif self.vtype == 'date': return parse_date(sentinel), parse_date(value) + elif self.vtype == 'duration': + return parse_duration(sentinel), parse_duration(value) elif self.vtype == 'age': if not isinstance(sentinel, datetime.datetime): sentinel = datetime.datetime.now(tz=tzutc()) - timedelta(sentinel) diff --git a/c7n/utils.py b/c7n/utils.py index cd16c4fa45c..60afcb00f2f 100644 --- a/c7n/utils.py +++ b/c7n/utils.py @@ -163,6 +163,46 @@ def parse_date(v, tz=None): return isinstance(v, datetime) and v or None +# ISO 8601 durations built from fixed-length units. Years and months +# vary in length, so a duration using them does not match. +ISO_DURATION = re.compile(r""" + P + (?:(?P\d+(?:[.,]\d+)?)W)? + (?:(?P\d+(?:[.,]\d+)?)D)? + (?:T(?=\d) + (?:(?P\d+(?:[.,]\d+)?)H)? + (?:(?P\d+(?:[.,]\d+)?)M)? + (?:(?P\d+(?:[.,]\d+)?)S)? + )? +""", re.VERBOSE | re.ASCII) + + +def parse_duration(v): + """Parse an ISO 8601 duration such as ``PT30M`` or ``P1DT12H`` to a timedelta. + + Weeks stand alone, only the smallest unit may have a fraction, and either + ``.`` or ``,`` marks the decimal. Returns None for anything else, including + durations in years or months and durations too large for a timedelta. + """ + if not isinstance(v, str): + return None + match = ISO_DURATION.fullmatch(v.strip()) + if match is None: + return None + parts = {unit: n for unit, n in match.groupdict().items() if n} + if not parts: + return None + # ISO 8601: weeks stand alone, and only the smallest unit given may have a fraction + if 'weeks' in parts and len(parts) > 1: + return None + if any(sep in n for n in list(parts.values())[:-1] for sep in '.,'): + return None + try: + return timedelta(**{unit: float(n.replace(',', '.')) for unit, n in parts.items()}) + except OverflowError: + return None + + if typing.TYPE_CHECKING: from c7n.element import ElementJSONSchema diff --git a/docs/source/filters.rst b/docs/source/filters.rst index cc114039c4a..85fa057decd 100644 --- a/docs/source/filters.rst +++ b/docs/source/filters.rst @@ -258,6 +258,11 @@ Value Type Transformations - ``size`` - the length of an element - ``swap`` - swap the value and the evaluated key - ``date`` - parse the filter's value as a date. + - ``duration`` - parse ISO 8601 durations such as ``PT30M`` or ``P1DT12H`` + (days, hours, minutes and seconds, or a week on its own; not years or months). + Only the smallest unit may have a fraction. Use with comparison operators such as + ``greater-than``. A resource value in any other format, such as ``P1Y`` or a + number of seconds, never matches. Note that the `age` and `expiration` transformations expect a value given as a number of days. Use a floating point value to match time periods shorter than @@ -308,6 +313,16 @@ Value Type Transformations value_type: age value: 0.5 + # Find Azure ML workspaces with a compute instance that stays idle for + # more than an hour before it shuts down + - type: compute-instances + attrs: + - type: value + key: properties.properties.idleTimeBeforeShutdown + op: greater-than + value_type: duration + value: PT1H + # Use `resource_count` to filter resources based on the number that matched # Note that no `key` is used for this value_type since it is matching on # the size of the list of resources and not a specific field. diff --git a/tests/test_filters.py b/tests/test_filters.py index 63cdf1574e3..38d4962b99d 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -586,6 +586,81 @@ def i(v): self.assertFilter(fdata, i("1.9.11"), True) self.assertFilter(fdata, i("1.1"), True) + def test_duration(self): + fdata = { + "type": "value", + "key": "IdleTimeBeforeShutdown", + "op": "greater-than", + "value_type": "duration", + "value": "PT1H", + } + + def i(d): + return instance(IdleTimeBeforeShutdown=d) + + self.assertFilter(fdata, i("PT120M"), True) + self.assertFilter(fdata, i("P1D"), True) + self.assertFilter(fdata, i("PT60M"), False) + self.assertFilter(fdata, i("PT30M"), False) + # values that aren't durations, or are missing, aren't greater than anything, + # and one too large for a timedelta doesn't raise + self.assertFilter(fdata, i("30 minutes"), False) + self.assertFilter(fdata, instance(), False) + self.assertFilter(fdata, i("P1000000000D"), False) + + # the same length written in different units is equal, with or without op + fdata["op"] = "equal" + self.assertFilter(fdata, i("PT60M"), True) + del fdata["op"] + self.assertFilter(fdata, i("PT60M"), True) + self.assertFilter(fdata, i("PT30M"), False) + + # nor are they unequal to anything + fdata["op"] = "not-equal" + self.assertFilter(fdata, i("PT30M"), True) + self.assertFilter(fdata, i("PT60M"), False) + self.assertFilter(fdata, i("30 minutes"), False) + self.assertFilter(fdata, instance(), False) + self.assertFilter(fdata, i("P1000000000D"), False) + + def test_duration_validation(self): + fdata = { + "type": "value", + "key": "IdleTimeBeforeShutdown", + "op": "greater-than", + "value_type": "duration", + "value": "1h", + } + self.assertRaises(PolicyValidationError, filters.factory(fdata, {}).validate) + + # years and months vary in length, so they are rejected + for value in ("P1Y", "P1M"): + fdata["value"] = value + self.assertRaisesRegex( + PolicyValidationError, "years and months", filters.factory(fdata, {}).validate) + + fdata["value"] = "PT1H" + filters.factory(fdata, {}).validate() + + # operators that can't compare two durations are rejected, before the value is checked + for op in ("regex", "in", "contains", "mod"): + fdata["op"] = op + self.assertRaisesRegex( + PolicyValidationError, "op: %s" % op, filters.factory(fdata, {}).validate) + fdata["op"] = "in" + fdata["value"] = ["PT1H", "PT2H"] + self.assertRaisesRegex( + PolicyValidationError, "op: in", filters.factory(fdata, {}).validate) + fdata["op"] = "less-than" + fdata["value"] = "PT1H" + filters.factory(fdata, {}).validate() + + # like value_type: date, a literal value is required + del fdata["value"] + fdata["value_path"] = "MaxIdleTime" + self.assertRaisesRegex( + PolicyValidationError, "literal value", filters.factory(fdata, {}).validate) + def test_expiration(self): now = datetime.now(tz=tz.tzutc()) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4919f2440db..15574faacb4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,7 +15,7 @@ from c7n import utils from c7n.config import Config from .common import BaseTest -from datetime import datetime +from datetime import datetime, timedelta class TestTesting(BaseTest): @@ -974,6 +974,34 @@ def test_parse_date_floor(): assert utils.parse_date('30') is None +@pytest.mark.parametrize('value, expected', [ + ('PT30M', timedelta(minutes=30)), + ('P1DT12H', timedelta(days=1, hours=12)), + ('P2W', timedelta(weeks=2)), + ('PT1.5H', timedelta(minutes=90)), + ('PT1,5H', timedelta(minutes=90)), + ('PT1M30S', timedelta(seconds=90)), + ('P1DT1.5H', timedelta(days=1, minutes=90)), + ('PT0S', timedelta(0)), + ('P0W', timedelta(0)), + (' PT1H\n', timedelta(hours=1)), +]) +def test_parse_duration(value, expected): + assert utils.parse_duration(value) == expected + + +# P1Y and P1M (a month) vary in length, so they're rejected, as are durations +# too large for a timedelta (including one that overflows to infinity) and +# non-ASCII digits. Weeks can't be combined with other units, and only the +# smallest unit may have a fraction. +@pytest.mark.parametrize('value', [ + 'P', 'PT', 'P1DT', 'PT1', 'P1Y', 'P1M', 'pt1h', '1h', None, 3600, + 'P1000000000D', 'P' + '9' * 20 + 'D', 'P' + '9' * 400 + 'D', 'PT١H', + 'P1W1D', 'P1WT1H', 'PT1.5H30M']) +def test_parse_duration_invalid(value): + assert utils.parse_duration(value) is None + + def test_output_path_join(): assert utils.join_output_path( 's3://cross-region-c7n/iam-check?region=us-east-2', diff --git a/tools/c7n_azure/tests_azure/cassettes/MachineLearningWorkspaceComputeIdleShutdownTest.test_idle_shutdown_duration.json b/tools/c7n_azure/tests_azure/cassettes/MachineLearningWorkspaceComputeIdleShutdownTest.test_idle_shutdown_duration.json new file mode 100644 index 00000000000..e2eafeab855 --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/MachineLearningWorkspaceComputeIdleShutdownTest.test_idle_shutdown_duration.json @@ -0,0 +1,342 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.MachineLearningServices/workspaces?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "content-length": [ + "12953" + ], + "date": [ + "Fri, 16 Oct 2026 19:21:37 GMT" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "x-msedge-ref": [ + "Ref A: 00000000000000000000000000000000 Ref B: 000000000000000 Ref C: 2026-10-16T19:21:37Z" + ], + "cache-control": [ + "no-cache" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "x-ms-original-request-ids": [ + "", + "", + "" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.MachineLearningServices/workspaces/cctestml000000000000", + "name": "cctestml000000000000", + "type": "Microsoft.MachineLearningServices/workspaces", + "location": "southcentralus", + "tags": {}, + "etag": null, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "description": "", + "storageAccount": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.Storage/storageAccounts/cctestmlst000000000000", + "keyVault": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.Keyvault/vaults/cctestmlkv000000000000", + "applicationInsights": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.insights/components/cctestmlai000000000000", + "hbiWorkspace": false, + "tenantId": "00000000-0000-0000-0000-000000000003", + "imageBuildCompute": null, + "provisioningState": "Succeeded", + "managedNetwork": { + "changeableIsolationModes": [ + "AllowInternetOutbound", + "AllowOnlyApprovedOutbound" + ], + "isolationMode": "Disabled", + "enableNetworkMonitor": false + }, + "allowRoleAssignmentOnRG": true, + "v1LegacyMode": false, + "softDeleteEnabled": false, + "creationTime": "2026-10-16T19:14:34.4587925Z", + "notebookInfo": { + "resourceId": "00000000000000000000000000000006", + "fqdn": "ml-cctestml-southcentralus-00000000-0000-0000-0000-000000000005.southcentralus.notebooks.azure.net", + "isPrivateLinkEnabled": false, + "notebookPreparationError": null + }, + "storageHnsEnabled": false, + "workspaceId": "00000000-0000-0000-0000-000000000005", + "linkedModelInventoryArmId": null, + "privateLinkCount": 0, + "publicNetworkAccess": "Enabled", + "discoveryUrl": "https://southcentralus.api.azureml.ms/discovery", + "mlFlowTrackingUri": "azureml://southcentralus.api.azureml.ms/mlflow/v1.0/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.MachineLearningServices/workspaces/cctestml000000000000", + "sdkTelemetryAppInsightsKey": "00000000-0000-0000-0000-000000000004", + "enableDataIsolation": false + }, + "identity": { + "type": "SystemAssigned", + "principalId": "00000000-0000-0000-0000-000000000002", + "tenantId": "00000000-0000-0000-0000-000000000003" + }, + "kind": "Default", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "systemData": { + "createdAt": "2026-10-16T19:14:34.4587925Z", + "createdBy": "user@example.com", + "createdByType": "User", + "lastModifiedAt": "2026-10-16T19:14:34.4587925Z", + "lastModifiedBy": "user@example.com", + "lastModifiedByType": "User" + } + } + ] + } + } + } + }, + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.MachineLearningServices/workspaces/cctestml000000000000/computes?api-version=2023-04-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "cache-control": [ + "no-cache" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "date": [ + "Fri, 16 Oct 2026 19:21:38 GMT" + ], + "x-request-time": [ + "0.261" + ], + "x-ms-response-type": [ + "standard" + ], + "x-msedge-ref": [ + "Ref A: 00000000000000000000000000000000 Ref B: 000000000000000 Ref C: 2026-10-16T19:21:38Z" + ], + "x-ms-ratelimit-remaining-subscription-global-reads": [ + "3749" + ], + "request-context": [ + "appId=cid-v1:00000000-0000-0000-0000-000000000007" + ], + "content-length": [ + "6278" + ], + "x-cache": [ + "CONFIG_NOCACHE" + ], + "azureml-served-by-cluster": [ + "vienna-southcentralus-02" + ] + }, + "body": { + "data": { + "value": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.MachineLearningServices/workspaces/cctestml000000000000/computes/cctest-ci-idle-60m", + "name": "cctest-ci-idle-60m", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-10-16T19:15:10.8772613+00:00", + "modifiedOn": "2026-10-16T19:19:58.454628+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "ComputeInstance", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_D2as_v4", + "imageVersion": null, + "imageSku": null, + "subnet": null, + "sshSettings": { + "sshPublicAccess": "Disabled", + "adminUserName": "azureuser", + "sshPort": 50000, + "adminPublicKey": null + }, + "autologgerSettings": null, + "errors": [], + "connectivityEndpoints": { + "publicIpAddress": "192.0.2.1", + "privateIpAddress": "10.0.0.4" + }, + "applications": [ + { + "displayName": "Jupyter", + "endpointUri": "https://cctest-ci-idle-60m.southcentralus.instances.azureml.ms/tree/" + }, + { + "displayName": "Jupyter Lab", + "endpointUri": "https://cctest-ci-idle-60m.southcentralus.instances.azureml.ms/lab" + } + ], + "state": "Running", + "lastOperation": { + "operationName": "Create", + "operationTime": "2026-10-16T19:15:19.222+00:00", + "operationStatus": "Succeeded", + "operationTrigger": "User" + }, + "createdBy": { + "userOrgId": "00000000-0000-0000-0000-000000000003", + "userId": "00000000-0000-0000-0000-000000000001" + }, + "applicationSharingPolicy": "Shared", + "computeInstanceAuthorizationType": null, + "personalComputeInstanceSettings": null, + "setupScripts": null, + "idleTimeBeforeShutdown": "PT60M", + "schedules": { + "computeStartStop": [] + }, + "enableBatchPrivateLink": false, + "enableOSPatching": false, + "releaseQuotaOnStop": false, + "enableSSO": true, + "dataMounts": null, + "dataDisks": null, + "containers": null, + "updateStatus": "InProgress", + "versions": { + "runtime": "3.0.03560.0001" + }, + "customServices": [], + "osImageMetadata": { + "currentImageVersion": "26.09.09", + "latestImageVersion": "26.09.09", + "isLatestOsImageVersion": true, + "osPatchingStatus": null + } + } + } + }, + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_machine-learning-compute-instance/providers/Microsoft.MachineLearningServices/workspaces/cctestml000000000000/computes/cctest-ci-idle-2h", + "name": "cctest-ci-idle-2h", + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "location": "southcentralus", + "tags": {}, + "properties": { + "createdOn": "2026-10-16T19:15:11.1144512+00:00", + "modifiedOn": "2026-10-16T19:19:58.3667919+00:00", + "disableLocalAuth": false, + "description": null, + "resourceId": null, + "computeType": "ComputeInstance", + "computeLocation": "southcentralus", + "provisioningState": "Succeeded", + "provisioningErrors": null, + "provisioningWarnings": {}, + "isAttachedCompute": false, + "properties": { + "vmSize": "Standard_D2as_v4", + "imageVersion": null, + "imageSku": null, + "subnet": null, + "sshSettings": { + "sshPublicAccess": "Disabled", + "adminUserName": "azureuser", + "sshPort": 50000, + "adminPublicKey": null + }, + "autologgerSettings": null, + "errors": [], + "connectivityEndpoints": { + "publicIpAddress": "192.0.2.2", + "privateIpAddress": "10.0.0.4" + }, + "applications": [ + { + "displayName": "Jupyter", + "endpointUri": "https://cctest-ci-idle-2h.southcentralus.instances.azureml.ms/tree/" + }, + { + "displayName": "Jupyter Lab", + "endpointUri": "https://cctest-ci-idle-2h.southcentralus.instances.azureml.ms/lab" + } + ], + "state": "Running", + "lastOperation": { + "operationName": "Create", + "operationTime": "2026-10-16T19:15:19.483+00:00", + "operationStatus": "Succeeded", + "operationTrigger": "User" + }, + "createdBy": { + "userOrgId": "00000000-0000-0000-0000-000000000003", + "userId": "00000000-0000-0000-0000-000000000001" + }, + "applicationSharingPolicy": "Shared", + "computeInstanceAuthorizationType": null, + "personalComputeInstanceSettings": null, + "setupScripts": null, + "idleTimeBeforeShutdown": "PT120M", + "schedules": { + "computeStartStop": [] + }, + "enableBatchPrivateLink": false, + "enableOSPatching": false, + "releaseQuotaOnStop": false, + "enableSSO": true, + "dataMounts": null, + "dataDisks": null, + "containers": null, + "updateStatus": "InProgress", + "versions": { + "runtime": "3.0.03560.0001" + }, + "customServices": [], + "osImageMetadata": { + "currentImageVersion": "26.09.09", + "latestImageVersion": "26.09.09", + "isLatestOsImageVersion": true, + "osPatchingStatus": null + } + } + } + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/templates/machine-learning-compute-instance.json b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-instance.json new file mode 100644 index 00000000000..d489b0eaa02 --- /dev/null +++ b/tools/c7n_azure/tests_azure/templates/machine-learning-compute-instance.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "uniqueId": { + "type": "string" + } + }, + "variables": { + "workspaceName": "[concat('cctestml', parameters('uniqueId'))]", + "storageName": "[concat('cctestmlst', parameters('uniqueId'))]", + "keyVaultName": "[concat('cctestmlkv', parameters('uniqueId'))]", + "appInsightsName": "[concat('cctestmlai', parameters('uniqueId'))]", + "computeInstances": [ + { + "name": "cctest-ci-idle-60m", + "idleTimeBeforeShutdown": "PT60M" + }, + { + "name": "cctest-ci-idle-2h", + "idleTimeBeforeShutdown": "PT2H" + } + ] + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2021-10-01", + "name": "[variables('keyVaultName')]", + "location": "[resourceGroup().location]", + "properties": { + "tenantId": "[subscription().tenantId]", + "sku": { + "family": "A", + "name": "standard" + }, + "accessPolicies": [], + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('appInsightsName')]", + "location": "[resourceGroup().location]", + "kind": "web", + "properties": { + "Application_Type": "web" + } + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-04-01", + "name": "[variables('workspaceName')]", + "location": "[resourceGroup().location]", + "identity": { + "type": "SystemAssigned" + }, + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "properties": { + "friendlyName": "cctest-machine-learning-workspace", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "applicationInsights": "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", + "publicNetworkAccess": "Enabled" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageName'))]", + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces/computes", + "apiVersion": "2025-12-01", + "name": "[format('{0}/{1}', variables('workspaceName'), variables('computeInstances')[copyIndex()].name)]", + "location": "[resourceGroup().location]", + "copy": { + "name": "computeInstanceCopy", + "count": "[length(variables('computeInstances'))]" + }, + "properties": { + "computeType": "ComputeInstance", + "properties": { + "vmSize": "Standard_D2as_v4", + "idleTimeBeforeShutdown": "[variables('computeInstances')[copyIndex()].idleTimeBeforeShutdown]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('workspaceName'))]" + ] + } + ] +} diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py index 2a2576e8615..64f7d30b40e 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning.py @@ -82,6 +82,52 @@ def test_additional_attributes(self): self.assertEqual(resources[0]['c7n:ComputeInstances'][0]['name'], 'vvmlwrkspc11') +class MachineLearningWorkspaceComputeIdleShutdownTest(BaseTest): + + recording_resource_group = 'test_machine-learning-compute-instance' + + @arm_template('machine-learning-compute-instance.json') + def test_idle_shutdown_duration(self): + p = self.load_policy({ + 'name': 'compute-instance-idle-shutdown-over-an-hour', + 'resource': 'azure.machine-learning-workspace', + 'filters': [ + { + 'type': 'value', + 'key': 'resourceGroup', + 'value_type': 'normalize', + 'value': self.recording_resource_group + }, + { + 'type': 'compute-instances', + 'attrs': [ + { + 'type': 'value', + 'key': 'properties.computeType', + 'value': 'ComputeInstance' + }, + { + 'type': 'value', + 'key': 'properties.properties.idleTimeBeforeShutdown', + 'op': 'greater-than', + 'value_type': 'duration', + 'value': 'PT1H' + }, + ] + }, + ], + }) + resources = p.run() + + assert len(resources) == 1 + # Azure stores the template's PT2H as PT120M. cctest-ci-idle-60m (PT60M) + # equals the threshold, so only the 2h instance matches. + assert [ + (c['name'], c['properties']['properties']['idleTimeBeforeShutdown']) + for c in resources[0]['c7n:ComputeInstances'] + ] == [('cctest-ci-idle-2h', 'PT120M')] + + class MachineLearningWorkspaceResourceLockFilterTest(BaseTest): def test_query(self): From d61e7e49524ce9f1e467bdf779c9569605c7bf2c Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Mon, 19 Oct 2026 21:44:50 -0400 Subject: [PATCH 54/65] aws - sagemaker-endpoint - add async inference metrics (#11194) Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: AJ Kerrigan Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- c7n/data/sagemaker_metrics.yaml | 54 ++- c7n/resources/sagemaker.py | 38 +- docs/source/aws/examples/sagemakermetrics.rst | 96 ++++- .../api.sagemaker.DescribeEndpoint_1.json | 63 ++++ .../api.sagemaker.DescribeEndpoint_2.json | 63 ++++ .../api.sagemaker.ListEndpoints_1.json | 58 +++ .../api.sagemaker.ListTags_1.json | 7 + .../api.sagemaker.ListTags_2.json | 7 + .../monitoring.GetMetricStatistics_1.json | 8 + .../monitoring.GetMetricStatistics_2.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../monitoring.GetMetricStatistics_1.json | 23 ++ .../README.md | 11 + .../sagemaker_endpoint_async_metrics/main.tf | 109 ++++++ .../model.tar.gz | Bin 0 -> 587 bytes .../tf_resources.json | 344 ++++++++++++++++++ .../probe_metrics.py | 35 +- tests/test_sagemaker.py | 245 ++++++++++++- 32 files changed, 1489 insertions(+), 40 deletions(-) create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListEndpoints_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_2.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_2.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ApproximateAgeOfOldestRequest/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSize/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSizePerInstance/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_CPUUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_DiskUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_HasBacklogWithoutCapacity/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_InvocationFailures/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_InvocationsProcessed/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_MemoryUtilization/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ModelLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_RequestDownloadFailures/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_RequestDownloadLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ResponseUploadFailures/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_ResponseUploadLatency/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_TimeInBacklog/monitoring.GetMetricStatistics_1.json create mode 100644 tests/data/placebo/test_sagemaker_metric_async_TotalProcessingTime/monitoring.GetMetricStatistics_1.json create mode 100644 tests/terraform/sagemaker_endpoint_async_metrics/README.md create mode 100644 tests/terraform/sagemaker_endpoint_async_metrics/main.tf create mode 100644 tests/terraform/sagemaker_endpoint_async_metrics/model.tar.gz create mode 100644 tests/terraform/sagemaker_endpoint_async_metrics/tf_resources.json diff --git a/c7n/data/sagemaker_metrics.yaml b/c7n/data/sagemaker_metrics.yaml index b427f47a685..ec94b7e6b27 100644 --- a/c7n/data/sagemaker_metrics.yaml +++ b/c7n/data/sagemaker_metrics.yaml @@ -9,11 +9,13 @@ # # kind names a variety of the resource, for metrics only some varieties # publish; leave it out when every variety publishes the section's metrics. -# Endpoints come in two kinds: a "classic" endpoint attaches a model to each +# Endpoints come in three kinds: a "classic" endpoint attaches a model to each # production variant, while an "inference-component" endpoint carries an # execution role and hosts models as components placed on a variant. The two # report invocations under different dimensions, which the documentation -# lists together without saying which applies when. +# lists together without saying which applies when. An "async" endpoint +# carries an AsyncInferenceConfig; its metrics have a page of their own: +# https://docs.aws.amazon.com/sagemaker/latest/dg/async-inference-monitor.html # # Only the metrics of resources c7n filters on are listed; the page also # covers jobs, Ground Truth, Feature Store and pipelines. @@ -119,3 +121,51 @@ - LoadedModelCount dimensions: - EndpointName, VariantName + +# The only real-time invocation metrics an async endpoint publishes. The page +# gives them no dimensions; the error counts' are by analogy with ModelLatency, +# since they weren't observed. +- title: Common Endpoint Metrics + resource: sagemaker-endpoint + kind: async + namespace: AWS/SageMaker + metrics: + - Invocation4XXErrors + - Invocation5XXErrors + - ModelLatency + dimensions: + - EndpointName, VariantName + +# The queue belongs to the endpoint rather than to a variant. +- title: Asynchronous Inference Endpoint Metrics + resource: sagemaker-endpoint + kind: async + namespace: AWS/SageMaker + metrics: + - ApproximateBacklogSize + - ApproximateBacklogSizePerInstance + - ApproximateAgeOfOldestRequest + - HasBacklogWithoutCapacity + dimensions: + - EndpointName + +# The page misspells InvocationsProcessed as InvocationsProcesssed. +# NotificationFailures and ExpiredRequests weren't observed, so their +# dimensions are the page's. +- title: Asynchronous Inference Endpoint Metrics + resource: sagemaker-endpoint + kind: async + namespace: AWS/SageMaker + metrics: + - RequestDownloadFailures + - ResponseUploadFailures + - NotificationFailures + - RequestDownloadLatency + - ResponseUploadLatency + - ExpiredRequests + - InvocationFailures + - InvocationsProcessed + - TimeInBacklog + - TotalProcessingTime + dimensions: + - EndpointName, VariantName diff --git a/c7n/resources/sagemaker.py b/c7n/resources/sagemaker.py index 3ae7138365b..ebdbe0d4af2 100644 --- a/c7n/resources/sagemaker.py +++ b/c7n/resources/sagemaker.py @@ -508,7 +508,8 @@ def resource_published_metric( We don't expect most resources to have different kinds, but endpoint do. None when this kind doesn't publish the metric: the resource has no series, rather than the policy being wrong, which - published_dimension_sets decides. + published_dimension_sets decides. process passes over such a + resource whatever the missing value. """ return ( SAGEMAKER_METRICS @@ -744,7 +745,32 @@ def process(self, resources, event=None): # fail on an undocumented metric name even if we weren't validated self.published_dimension_sets() - return super().process(resources, event) + # Classified before the base filter fans out to threads, so a kind + # needing an api call to decide makes it once rather than per chunk. + published = [] + skipped = collections.Counter() + for resource in resources: + kind = self.resource_kind(resource) + if kind is not None: + resource['c7n:metrics-kind'] = kind + if self.resource_published_metric(resource) is None: + skipped[kind] += 1 + else: + published.append(resource) + + if skipped: + # A kind that doesn't publish the metric has no series to read + # as missing, so the missing value doesn't decide it. That only + # changes the outcome when there is a missing value, which is + # when a policy author needs telling. + log = self.log.warning if 'missing-value' in self.data else self.log.debug + log("%s skipped %d of %d %s whose kind doesn't publish %s: %s", + self.type, sum(skipped.values()), len(resources), + self.manager.type, self.data['name'], + ', '.join(f"{count} {kind}" + for kind, count in sorted(skipped.items()))) + + return super().process(published, event) @SagemakerEndpoint.filter_registry.register('metrics') @@ -758,6 +784,7 @@ class SagemakerEndpointMetricsFilter(SageMakerMetricsFilter): VariantName = "VariantName" ProductionVariants = "ProductionVariants" InferenceComponentName = "InferenceComponentName" + AsyncInferenceConfig = "AsyncInferenceConfig" permissions = MetricsFilter.permissions + ( 'sagemaker:ListInferenceComponents',) @@ -766,7 +793,7 @@ class SagemakerEndpointMetricsFilter(SageMakerMetricsFilter): def endpoint_components(self): """Map each endpoint to the inference components hosted on it. - An endpoint without inference components is assumed to be a classic endpoint. + An endpoint without inference components is assumed to be classic or async. """ client = local_session( self.manager.session_factory).client('sagemaker') @@ -781,6 +808,9 @@ def endpoint_components(self): def resource_kind(self, resource) -> Kind: """How this endpoint hosts its models. + An async endpoint is classified first, since it publishes the async + metrics; DescribeEndpoint already carries its AsyncInferenceConfig. + An endpoint built to host components but hosting none right now is reported classic, which costs nothing: its invocations aren't published per variant, and nothing is reserving the instance, so @@ -789,6 +819,8 @@ def resource_kind(self, resource) -> Kind: execution role and no variant naming a model -- would classify it correctly at the price of a DescribeEndpointConfig per endpoint. """ + if self.AsyncInferenceConfig in resource: + return 'async' if self.endpoint_components.get(resource[self.resource_dimension_name]): return 'inference-component' return 'classic' diff --git a/docs/source/aws/examples/sagemakermetrics.rst b/docs/source/aws/examples/sagemakermetrics.rst index aea3d5d2d56..15fc88c6130 100644 --- a/docs/source/aws/examples/sagemakermetrics.rst +++ b/docs/source/aws/examples/sagemakermetrics.rst @@ -43,9 +43,11 @@ request arrived, but no ``CPUUtilization`` value means nothing was measured, which is not the same as nothing being used. Whether a metric behaves this way is up to the service publishing it, and -endpoints don't: an endpoint reports a zero for an interval nothing called -it, so a policy looking for idle endpoints needs no missing value. See -`Endpoints that serve no traffic`_ for when one is called for. +real-time endpoints don't: one reports a zero for an interval nothing +called it, so a policy looking for idle real-time endpoints needs no +missing value. An asynchronous endpoint can have no +``InvocationsProcessed`` at all: one that hasn't processed a request has none. +See `Endpoints that serve no traffic`_. Filtering which metric data for a resource is considered -------------------------------------------------------- @@ -88,19 +90,22 @@ nothing to measure. SageMaker Endpoints ------------------- -There are two kinds of endpoints: +There are three kinds of endpoints: - Classic endpoints that deploy models in variants - Inference-component endpoints that deploy models in inference components in variants +- Asynchronous endpoints, whose configuration has an + ``AsyncInferenceConfig`` + Which dimensions you can supply depends on the metric, and for invocations on the kind of endpoint as well. Utilization metrics -- ``CPUUtilization`` and the rest -- are reported per variant whichever kind of endpoint publishes them, so ``VariantName`` -narrows them in both cases: +narrows them in every case: .. code-block:: yaml @@ -108,7 +113,7 @@ narrows them in both cases: VariantName: gpu Invocations and latencies are reported per variant by a classic endpoint -and per component by a component endpoint, so the two kinds take different +and per component by a component endpoint, so those two kinds take different dimensions: ``VariantName`` @@ -119,19 +124,34 @@ dimensions: ``InferenceComponentName`` Narrows a component endpoint's invocations to one component. Naming one - leaves every classic endpoint with nothing to measure, and the same - applies to them. + leaves every classic or asynchronous endpoint with nothing to measure, + and the same applies to them. Either way, an endpoint with nothing to measure is decided by the ``missing-value``, if the policy gives one, exactly as an endpoint whose series reported nothing would be. +Asynchronous endpoints +~~~~~~~~~~~~~~~~~~~~~~ + +Of the real-time invocation metrics, an asynchronous endpoint publishes +only ``Invocation4XXErrors``, ``Invocation5XXErrors`` and ``ModelLatency``; +it counts requests in ``InvocationsProcessed`` instead. `Asynchronous +inference metrics +`_ +lists the rest, and misspells it ``InvocationsProcesssed``. + +The queue belongs to the endpoint rather than a variant, so +``ApproximateBacklogSize``, ``ApproximateBacklogSizePerInstance``, +``ApproximateAgeOfOldestRequest`` and ``HasBacklogWithoutCapacity`` take no +``VariantName``. The rest are reported per variant. + Endpoints that serve no traffic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -An endpoint bills for its instances from creation until it is deleted, -whether or not anything calls it. +An endpoint bills for the instances it holds, whether or not anything +calls it. .. code-block:: yaml @@ -150,14 +170,56 @@ whether or not anything calls it. value: 0 op: lte -An endpoint reports a zero for an interval in which nothing called it, -whichever way it hosts its models, so its own values answer the question -and no ``missing-value`` is needed here. +A real-time endpoint reports a zero for an interval in which nothing called +it, whichever way it hosts its models, so its own values answer the +question and no ``missing-value`` is needed here. + +Supply one when a metric may have no values at all, such as for an +endpoint created part way through the window. Then ``missing-value: 0`` +reads the absence as a zero, and leaving it out passes the endpoint over. + +A metric its kind of endpoint doesn't publish is different: there is no +series to be missing, so the endpoint is skipped regardless of the +``missing-value`` setting. ``InvocationsPerInstance`` with ``missing-value: 0`` +looks only at classic endpoints, rather than reporting every component +endpoint as idle. Given a ``missing-value``, the filter logs a warning +when it passes endpoints over this way. Either way it records the kind it +decided on each endpoint as ``c7n:metrics-kind``. + +The policy above passes over asynchronous endpoints, which publish no +``Invocations``. Their ``InvocationsProcessed`` needs ``missing-value: 0``, +which leaves the real-time endpoints alone, since they don't publish it: -Supply one when a metric may have no values at all: for an endpoint -created part way through the window, or for a metric its kind of endpoint -doesn't publish. Then ``missing-value: 0`` reads the absence as a zero, -and leaving it out passes the endpoint over. +.. code-block:: yaml + + policies: + - name: sagemaker-endpoints-idle-including-async + resource: aws.sagemaker-endpoint + description: | + In-service endpoints, real-time or async, that processed no + requests in the last week + filters: + - EndpointStatus: InService + - or: + - type: metrics + name: Invocations + statistics: Sum + days: 7 + period: 86400 + value: 0 + op: lte + - type: metrics + name: InvocationsProcessed + statistics: Sum + days: 7 + period: 86400 + value: 0 + op: lte + missing-value: 0 + +An async endpoint whose requests are still queued, such as one scaled to +zero instances, has processed none either; ``ApproximateBacklogSize`` shows +whether any are waiting. Endpoints with under-used GPUs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_1.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_1.json new file mode 100644 index 00000000000..ce773f116ca --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_1.json @@ -0,0 +1,63 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-async-concrete-crayfish-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-idle", + "EndpointConfigName": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 14, + "microsecond": 766000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 14, + "microsecond": 74000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 9, + "second": 0, + "microsecond": 350000 + }, + "AsyncInferenceConfig": { + "OutputConfig": { + "S3OutputPath": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/responses" + } + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_2.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_2.json new file mode 100644 index 00000000000..8072a15774b --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.DescribeEndpoint_2.json @@ -0,0 +1,63 @@ +{ + "status_code": 200, + "data": { + "EndpointName": "c7n-endpoint-metrics-async-concrete-crayfish-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-busy", + "EndpointConfigName": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "ProductionVariants": [ + { + "VariantName": "AllTraffic", + "DeployedImages": [ + { + "SpecifiedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "ResolvedImage": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost@sha256:b4f13edb198529c460692015797fa1ca6a8ff1ed64a149297174d922121b8fc4", + "ResolutionTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 13, + "microsecond": 610000 + } + } + ], + "CurrentWeight": 1.0, + "DesiredWeight": 1.0, + "CurrentInstanceCount": 1, + "DesiredInstanceCount": 1 + } + ], + "EndpointStatus": "InService", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 12, + "microsecond": 603000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 8, + "second": 54, + "microsecond": 226000 + }, + "AsyncInferenceConfig": { + "OutputConfig": { + "S3OutputPath": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/responses" + } + }, + "MetricsConfig": { + "EnableDetailedObservability": true + }, + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListEndpoints_1.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListEndpoints_1.json new file mode 100644 index 00000000000..ba80b9c11c6 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListEndpoints_1.json @@ -0,0 +1,58 @@ +{ + "status_code": 200, + "data": { + "Endpoints": [ + { + "EndpointName": "c7n-endpoint-metrics-async-concrete-crayfish-idle", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-idle", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 14, + "microsecond": 74000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 9, + "second": 0, + "microsecond": 350000 + }, + "EndpointStatus": "InService" + }, + { + "EndpointName": "c7n-endpoint-metrics-async-concrete-crayfish-busy", + "EndpointArn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-busy", + "CreationTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 5, + "second": 12, + "microsecond": 603000 + }, + "LastModifiedTime": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 17, + "minute": 8, + "second": 54, + "microsecond": 226000 + }, + "EndpointStatus": "InService" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_1.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_1.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_1.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_2.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_2.json new file mode 100644 index 00000000000..e41b16b3e98 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/api.sagemaker.ListTags_2.json @@ -0,0 +1,7 @@ +{ + "status_code": 200, + "data": { + "Tags": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..78e42887bf0 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,8 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsProcessed", + "Datapoints": [], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_2.json b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_2.json new file mode 100644 index 00000000000..1731fa1a757 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_endpoint_async_metrics_idle/monitoring.GetMetricStatistics_2.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsProcessed", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 6, + "hour": 17, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Sum": 5.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ApproximateAgeOfOldestRequest/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ApproximateAgeOfOldestRequest/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..5b78614edfc --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ApproximateAgeOfOldestRequest/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ApproximateAgeOfOldestRequest", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Seconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSize/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSize/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..4abeddd6fee --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSize/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ApproximateBacklogSize", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSizePerInstance/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSizePerInstance/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..2fcb8726a5d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ApproximateBacklogSizePerInstance/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ApproximateBacklogSizePerInstance", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_CPUUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_CPUUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..ff143c3b53e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_CPUUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "CPUUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.06300277347927774, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_DiskUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_DiskUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..df3c57e7a8f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_DiskUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "DiskUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 22.3593, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_HasBacklogWithoutCapacity/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_HasBacklogWithoutCapacity/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..b1435af390e --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_HasBacklogWithoutCapacity/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "HasBacklogWithoutCapacity", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_InvocationFailures/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_InvocationFailures/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..a9b184dc28f --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_InvocationFailures/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationFailures", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_InvocationsProcessed/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_InvocationsProcessed/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..31207e3ae22 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_InvocationsProcessed/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "InvocationsProcessed", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 1.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_MemoryUtilization/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_MemoryUtilization/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..1fec9cbf558 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_MemoryUtilization/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "MemoryUtilization", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 10.609637290167866, + "Unit": "Percent" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ModelLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ModelLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..a164687db88 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ModelLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ModelLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 1605.2, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadFailures/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadFailures/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..c59ffc4c8c5 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadFailures/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "RequestDownloadFailures", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..f08dbce84dd --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_RequestDownloadLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "RequestDownloadLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 241737.2, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadFailures/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadFailures/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..c5cc68025fe --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadFailures/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ResponseUploadFailures", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 0.0, + "Unit": "Count" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadLatency/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadLatency/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..7b6b91a2644 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_ResponseUploadLatency/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "ResponseUploadLatency", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 112656.0, + "Unit": "Microseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_TimeInBacklog/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_TimeInBacklog/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..8b661da0a9d --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_TimeInBacklog/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "TimeInBacklog", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 472.4, + "Unit": "Milliseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/data/placebo/test_sagemaker_metric_async_TotalProcessingTime/monitoring.GetMetricStatistics_1.json b/tests/data/placebo/test_sagemaker_metric_async_TotalProcessingTime/monitoring.GetMetricStatistics_1.json new file mode 100644 index 00000000000..a19a35bcc77 --- /dev/null +++ b/tests/data/placebo/test_sagemaker_metric_async_TotalProcessingTime/monitoring.GetMetricStatistics_1.json @@ -0,0 +1,23 @@ +{ + "status_code": 200, + "data": { + "Label": "TotalProcessingTime", + "Datapoints": [ + { + "Timestamp": { + "__class__": "datetime", + "year": 2026, + "month": 10, + "day": 7, + "hour": 16, + "minute": 15, + "second": 0, + "microsecond": 0 + }, + "Average": 889.2, + "Unit": "Milliseconds" + } + ], + "ResponseMetadata": {} + } +} \ No newline at end of file diff --git a/tests/terraform/sagemaker_endpoint_async_metrics/README.md b/tests/terraform/sagemaker_endpoint_async_metrics/README.md new file mode 100644 index 00000000000..1b8d84146d3 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_async_metrics/README.md @@ -0,0 +1,11 @@ +# sagemaker_endpoint_async_metrics + +Two asynchronous inference endpoints sharing one model and one endpoint +configuration: `busy` is invoked when recording and `idle` never is. + +This is a fixture of its own rather than more endpoints in +`sagemaker_endpoint_metrics`, because recording against that fixture renames +every endpoint in it, and with them every recording made against it. + +`model.tar.gz` is a copy of the one in `../sagemaker_endpoint_metrics`, whose +README says how it's built. diff --git a/tests/terraform/sagemaker_endpoint_async_metrics/main.tf b/tests/terraform/sagemaker_endpoint_async_metrics/main.tf new file mode 100644 index 00000000000..02b0b455462 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_async_metrics/main.tf @@ -0,0 +1,109 @@ +provider "aws" {} + +resource "random_pet" "main" { + length = 2 + separator = "-" +} + +locals { + # under probe_metrics.py's prefix, so the probe finds these endpoints too + name = "c7n-endpoint-metrics-async-${random_pet.main.id}" +} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["sagemaker.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "execution" { + name = local.name + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_iam_role_policy_attachment" "execution" { + role = aws_iam_role.execution.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess" +} + +# the bucket name has to contain "sagemaker" for AmazonSageMakerFullAccess to +# let the execution role read the model and requests, and write responses +resource "aws_s3_bucket" "main" { + bucket = "c7n-sagemaker-async-metrics-${random_pet.main.id}" + force_destroy = true +} + +resource "aws_s3_object" "model" { + bucket = aws_s3_bucket.main.id + key = "model.tar.gz" + source = "${path.module}/model.tar.gz" + etag = filemd5("${path.module}/model.tar.gz") +} + +# the tests pass this request by S3 location rather than inline +resource "aws_s3_object" "request" { + bucket = aws_s3_bucket.main.id + key = "request.csv" + content = "1.0\n" +} + +data "aws_sagemaker_prebuilt_ecr_image" "xgboost" { + repository_name = "sagemaker-xgboost" + image_tag = "1.7-1" +} + +resource "aws_sagemaker_model" "main" { + name = local.name + execution_role_arn = aws_iam_role.execution.arn + + # the arn alone orders this after the role, not after its policy, and an + # endpoint whose role can't yet read the artifact fails to come up + depends_on = [aws_iam_role_policy_attachment.execution] + + primary_container { + image = data.aws_sagemaker_prebuilt_ecr_image.xgboost.registry_path + model_data_url = "s3://${aws_s3_bucket.main.id}/${aws_s3_object.model.key}" + } +} + +# async_inference_config is what makes both endpoints async +resource "aws_sagemaker_endpoint_configuration" "async" { + name_prefix = "c7n-em-async-" + + lifecycle { + create_before_destroy = true + } + + production_variants { + variant_name = "AllTraffic" + model_name = aws_sagemaker_model.main.name + initial_instance_count = 1 + instance_type = "ml.c5.large" + } + + async_inference_config { + output_config { + s3_output_path = "s3://${aws_s3_bucket.main.id}/responses" + } + } +} + +# invoked when recording +resource "aws_sagemaker_endpoint" "busy" { + name = "${local.name}-busy" + endpoint_config_name = aws_sagemaker_endpoint_configuration.async.name +} + +# never invoked +resource "aws_sagemaker_endpoint" "idle" { + name = "${local.name}-idle" + endpoint_config_name = aws_sagemaker_endpoint_configuration.async.name +} + +output "request_location" { + value = "s3://${aws_s3_bucket.main.id}/${aws_s3_object.request.key}" +} diff --git a/tests/terraform/sagemaker_endpoint_async_metrics/model.tar.gz b/tests/terraform/sagemaker_endpoint_async_metrics/model.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6ccfd873df8cf521c4612b136818d3763ece13da GIT binary patch literal 587 zcmV-R0<`@fiwFP!000001MQSyZ<{a>hVxS*es6%){x_XRvO9lkWx@YV&Wl&M}Q#-p24 zZ~!XSQD^ORM^*Zvi$U9BmNtlkkRpkB3Vn?6b%Q!|Ft%5wug(EA9n(bOL9wdoYfG6a zCJk3{(jL@TFwIjGwMie+WK(?FJ#4aMeBgGx4m8%8`R;GA>QU;i+52lQ7tHs0dj+)~ z+MrsfLllaCm^PI-q=}%cDxXYSIp}d&CL8+44XyPaS9IURN*Rfi3F*cUu4AXBqcfQo z`RDCEFQ(d#uDPt5y;k+-*5H!uD?<&W*X8e8G48|mnlc_g%O3OOk7W`E^aS1;9S_5L za{SCAxf?Og9MBFp%yb&=nD3o*Ta&i$>WT$rQ0M&f8YM~&k4gY48t%C!!QiPFbu;m Z48t%C!!QiPFbwmT`2h~am$d*W003_nBzyn> literal 0 HcmV?d00001 diff --git a/tests/terraform/sagemaker_endpoint_async_metrics/tf_resources.json b/tests/terraform/sagemaker_endpoint_async_metrics/tf_resources.json new file mode 100644 index 00000000000..9e336d4dd84 --- /dev/null +++ b/tests/terraform/sagemaker_endpoint_async_metrics/tf_resources.json @@ -0,0 +1,344 @@ +{ + "pytest-terraform": 1, + "outputs": { + "request_location": { + "value": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/request.csv", + "type": "string" + } + }, + "resources": { + "aws_iam_policy_document": { + "assume_role": { + "id": "1147345228", + "json": "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Effect\": \"Allow\",\n \"Action\": \"sts:AssumeRole\",\n \"Principal\": {\n \"Service\": \"sagemaker.amazonaws.com\"\n }\n }\n ]\n}", + "minified_json": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"sts:AssumeRole\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}]}", + "override_json": null, + "override_policy_documents": null, + "policy_id": null, + "source_json": null, + "source_policy_documents": null, + "statement": [ + { + "actions": [ + "sts:AssumeRole" + ], + "condition": [], + "effect": "Allow", + "not_actions": [], + "not_principals": [], + "not_resources": [], + "principals": [ + { + "identifiers": [ + "sagemaker.amazonaws.com" + ], + "type": "Service" + } + ], + "resources": [], + "sid": "" + } + ], + "version": "2012-10-17" + } + }, + "aws_sagemaker_prebuilt_ecr_image": { + "xgboost": { + "dns_suffix": null, + "id": "644160558196", + "image_tag": "1.7-1", + "region": "us-east-1", + "registry_id": "644160558196", + "registry_path": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "repository_name": "sagemaker-xgboost" + } + }, + "aws_iam_role": { + "execution": { + "arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-async-concrete-crayfish", + "assume_role_policy": "{\"Statement\":[{\"Action\":\"sts:AssumeRole\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"sagemaker.amazonaws.com\"}}],\"Version\":\"2012-10-17\"}", + "create_date": "2026-10-07T21:05:00Z", + "description": "", + "force_detach_policies": false, + "id": "c7n-endpoint-metrics-async-concrete-crayfish", + "inline_policy": [], + "managed_policy_arns": [], + "max_session_duration": 3600, + "name": "c7n-endpoint-metrics-async-concrete-crayfish", + "name_prefix": "", + "path": "/", + "permissions_boundary": "", + "tags": null, + "tags_all": {}, + "unique_id": "AROAEXAMPLEROLEID1" + } + }, + "aws_iam_role_policy_attachment": { + "execution": { + "id": "c7n-endpoint-metrics-async-concrete-crayfish/arn:aws:iam::aws:policy/AmazonSageMakerFullAccess", + "policy_arn": "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess", + "role": "c7n-endpoint-metrics-async-concrete-crayfish" + } + }, + "aws_s3_bucket": { + "main": { + "acceleration_status": "", + "acl": null, + "arn": "arn:aws:s3:::c7n-sagemaker-async-metrics-concrete-crayfish", + "bucket": "c7n-sagemaker-async-metrics-concrete-crayfish", + "bucket_domain_name": "c7n-sagemaker-async-metrics-concrete-crayfish.s3.amazonaws.com", + "bucket_namespace": "global", + "bucket_prefix": "", + "bucket_region": "us-east-1", + "bucket_regional_domain_name": "c7n-sagemaker-async-metrics-concrete-crayfish.s3.us-east-1.amazonaws.com", + "cors_rule": [], + "force_destroy": true, + "grant": [ + { + "id": "0000000000000000000000000000000000000000000000000000000000000000", + "permissions": [ + "FULL_CONTROL" + ], + "type": "CanonicalUser", + "uri": "" + } + ], + "hosted_zone_id": "Z3AQBSTGFYJSTF", + "id": "c7n-sagemaker-async-metrics-concrete-crayfish", + "lifecycle_rule": [], + "logging": [], + "object_lock_configuration": [], + "object_lock_enabled": false, + "policy": "", + "region": "us-east-1", + "replication_configuration": [], + "request_payer": "BucketOwner", + "server_side_encryption_configuration": [ + { + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "kms_master_key_id": "", + "sse_algorithm": "AES256" + } + ], + "bucket_key_enabled": false + } + ] + } + ], + "tags": null, + "tags_all": {}, + "timeouts": null, + "versioning": [ + { + "enabled": false, + "mfa_delete": false + } + ], + "website": [], + "website_domain": null, + "website_endpoint": null + } + }, + "aws_s3_object": { + "model": { + "acl": null, + "arn": "arn:aws:s3:::c7n-sagemaker-async-metrics-concrete-crayfish/model.tar.gz", + "bucket": "c7n-sagemaker-async-metrics-concrete-crayfish", + "bucket_key_enabled": false, + "cache_control": "", + "checksum_algorithm": null, + "checksum_crc32": "", + "checksum_crc32c": "", + "checksum_crc64nvme": "", + "checksum_sha1": "", + "checksum_sha256": "", + "content": null, + "content_base64": null, + "content_disposition": "", + "content_encoding": "", + "content_language": "", + "content_type": "application/octet-stream", + "etag": "dd8ee194115d990ad16e3ddaf17bf982", + "force_destroy": false, + "id": "c7n-sagemaker-async-metrics-concrete-crayfish/model.tar.gz", + "key": "model.tar.gz", + "kms_key_id": null, + "metadata": null, + "object_lock_legal_hold_status": "", + "object_lock_mode": "", + "object_lock_retain_until_date": "", + "override_provider": [], + "region": "us-east-1", + "server_side_encryption": "AES256", + "source": "./model.tar.gz", + "source_hash": null, + "storage_class": "STANDARD", + "tags": null, + "tags_all": {}, + "version_id": "", + "website_redirect": "" + }, + "request": { + "acl": null, + "arn": "arn:aws:s3:::c7n-sagemaker-async-metrics-concrete-crayfish/request.csv", + "bucket": "c7n-sagemaker-async-metrics-concrete-crayfish", + "bucket_key_enabled": false, + "cache_control": "", + "checksum_algorithm": null, + "checksum_crc32": "", + "checksum_crc32c": "", + "checksum_crc64nvme": "", + "checksum_sha1": "", + "checksum_sha256": "", + "content": "1.0\n", + "content_base64": null, + "content_disposition": "", + "content_encoding": "", + "content_language": "", + "content_type": "application/octet-stream", + "etag": "2542b79651fab56d0ebfff75a0fdf7be", + "force_destroy": false, + "id": "c7n-sagemaker-async-metrics-concrete-crayfish/request.csv", + "key": "request.csv", + "kms_key_id": null, + "metadata": null, + "object_lock_legal_hold_status": "", + "object_lock_mode": "", + "object_lock_retain_until_date": "", + "override_provider": [], + "region": "us-east-1", + "server_side_encryption": "AES256", + "source": null, + "source_hash": null, + "storage_class": "STANDARD", + "tags": null, + "tags_all": {}, + "version_id": "", + "website_redirect": "" + } + }, + "aws_sagemaker_endpoint": { + "busy": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-busy", + "deployment_config": [], + "endpoint_config_name": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "id": "c7n-endpoint-metrics-async-concrete-crayfish-busy", + "name": "c7n-endpoint-metrics-async-concrete-crayfish-busy", + "region": "us-east-1", + "tags": null, + "tags_all": {} + }, + "idle": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint/c7n-endpoint-metrics-async-concrete-crayfish-idle", + "deployment_config": [], + "endpoint_config_name": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "id": "c7n-endpoint-metrics-async-concrete-crayfish-idle", + "name": "c7n-endpoint-metrics-async-concrete-crayfish-idle", + "region": "us-east-1", + "tags": null, + "tags_all": {} + } + }, + "aws_sagemaker_endpoint_configuration": { + "async": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:endpoint-config/c7n-em-async-f29f6039b890a7b1e442e2d538", + "async_inference_config": [ + { + "client_config": [], + "output_config": [ + { + "kms_key_id": "", + "notification_config": [], + "s3_failure_path": "", + "s3_output_path": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/responses" + } + ] + } + ], + "data_capture_config": [], + "execution_role_arn": "", + "id": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "kms_key_arn": "", + "name": "c7n-em-async-f29f6039b890a7b1e442e2d538", + "name_prefix": "c7n-em-async-", + "production_variants": [ + { + "accelerator_type": "", + "capacity_reservation_config": [], + "container_startup_health_check_timeout_in_seconds": 0, + "core_dump_config": [], + "enable_ssm_access": false, + "inference_ami_version": "", + "initial_instance_count": 1, + "initial_variant_weight": 1, + "instance_type": "ml.c5.large", + "managed_instance_scaling": [], + "model_data_download_timeout_in_seconds": 0, + "model_name": "c7n-endpoint-metrics-async-concrete-crayfish", + "routing_config": [], + "serverless_config": [], + "variant_name": "AllTraffic", + "volume_size_in_gb": 0 + } + ], + "region": "us-east-1", + "shadow_production_variants": [], + "tags": null, + "tags_all": {} + } + }, + "aws_sagemaker_model": { + "main": { + "arn": "arn:aws:sagemaker:us-east-1:644160558196:model/c7n-endpoint-metrics-async-concrete-crayfish", + "container": [], + "enable_network_isolation": false, + "execution_role_arn": "arn:aws:iam::644160558196:role/c7n-endpoint-metrics-async-concrete-crayfish", + "id": "c7n-endpoint-metrics-async-concrete-crayfish", + "inference_execution_config": [], + "name": "c7n-endpoint-metrics-async-concrete-crayfish", + "primary_container": [ + { + "additional_model_data_source": [], + "container_hostname": "", + "environment": null, + "image": "644160558196.dkr.ecr.us-east-1.amazonaws.com/sagemaker-xgboost:1.7-1", + "image_config": [], + "inference_specification_name": "", + "mode": "SingleModel", + "model_data_source": [ + { + "s3_data_source": [ + { + "compression_type": "Gzip", + "model_access_config": [], + "s3_data_type": "S3Object", + "s3_uri": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/model.tar.gz" + } + ] + } + ], + "model_data_url": "s3://c7n-sagemaker-async-metrics-concrete-crayfish/model.tar.gz", + "model_package_name": "", + "multi_model_config": [] + } + ], + "region": "us-east-1", + "tags": null, + "tags_all": {}, + "vpc_config": [] + } + }, + "random_pet": { + "main": { + "id": "concrete-crayfish", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py b/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py index 6df3c7ec9d2..eb74375572d 100644 --- a/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py +++ b/tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py @@ -14,6 +14,8 @@ terraform apply AWS_DEFAULT_REGION=us-east-1 uv run tests/terraform/sagemaker_endpoint_metrics/probe_metrics.py +Apply ../sagemaker_endpoint_async_metrics as well to probe async endpoints. + Everything goes to standard output. Use --no-invoke to skip generating traffic when the endpoints have been invoked within the metric window. @@ -25,6 +27,7 @@ The documentation is the only source of metric *names* and namespaces: https://docs.aws.amazon.com/sagemaker/latest/dg/monitoring-cloudwatch.html + https://docs.aws.amazon.com/sagemaker/latest/dg/async-inference-monitor.html Fetch it as markdown -- the same URL with .md instead of .html serves the tables without the page chrome. Each metric table becomes a section whose @@ -50,8 +53,9 @@ And for `kind`, `classic` means a model is attached to each production variant, `inference-component` means the configuration carries an -execution role and models arrive as components. A section's metrics belong -to the kinds this script saw publishing them. +execution role and models arrive as components, and `async` means the +endpoint carries an AsyncInferenceConfig. A section's metrics belong to the +kinds this script saw publishing them. The script cannot see everything. It reports which catalogue entries it could not check -- metrics no endpoint here publishes, such as the GPU @@ -86,9 +90,12 @@ def endpoint_kind(sagemaker, endpoint: dict) -> str: """Which way this endpoint hosts its models. - The rule the SageMaker SDK uses: an execution role on the - configuration and no production variant naming a model. + An async endpoint says so on the endpoint itself. Otherwise, the rule + the SageMaker SDK uses: an execution role on the configuration and no + production variant naming a model. """ + if 'AsyncInferenceConfig' in endpoint: + return 'async' config = sagemaker.describe_endpoint_config( EndpointConfigName=endpoint['EndpointConfigName']) if config.get('ExecutionRoleArn') and not any( @@ -123,13 +130,27 @@ def find_components(sagemaker, endpoints: list[dict]) -> dict[str, list[str]]: def invoke(runtime, endpoints, components, count: int) -> None: - """Invoke one variant of each classic endpoint, and each component. + """Invoke one variant of each classic endpoint, each component, and + each async endpoint. A variant named "quiet" is left alone: a variant that is never invoked - still publishes zeros, which is worth seeing. + still publishes zeros, which is worth seeing. So is an async endpoint + named "-idle", to see what one that has processed nothing publishes. """ for endpoint in endpoints: name = endpoint['EndpointName'] + if endpoint['kind'] == 'async': + if name.endswith('-idle'): + continue + # the request main.tf uploads beside the endpoint's responses + output = endpoint['AsyncInferenceConfig']['OutputConfig']['S3OutputPath'] + request = output.rsplit('/', 1)[0] + '/request.csv' + for _ in range(count): + runtime.invoke_endpoint_async( + EndpointName=name, ContentType='text/csv', + InputLocation=request) + print(f" invoked {name} {count}x asynchronously") + continue targets = [ dict(InferenceComponentName=component) for component in components.get(name, ()) @@ -169,6 +190,8 @@ def observed(cloudwatch, names: set[str]) -> dict: def fill(name_set: list[str], endpoint: dict, component: str | None) -> list[list[dict]]: """The dimensions to query for one resource, or [] if we can't supply them.""" + if name_set == ['EndpointName']: + return [[{'Name': 'EndpointName', 'Value': endpoint['EndpointName']}]] if name_set == ['EndpointName', 'VariantName']: return [ [{'Name': 'EndpointName', 'Value': endpoint['EndpointName']}, diff --git a/tests/test_sagemaker.py b/tests/test_sagemaker.py index 3525413d3a9..49bf18b0a21 100644 --- a/tests/test_sagemaker.py +++ b/tests/test_sagemaker.py @@ -1,5 +1,6 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +import logging import time from unittest import mock @@ -9,7 +10,6 @@ from .apicallcaptor import ApiCallCaptor from .common import BaseTest -from c7n.filters.metrics import MetricsFilter from c7n.resources.sagemaker import ( SAGEMAKER_METRICS, SagemakerEndpoint, SagemakerJobQueryParser, CompilationJobQueryParser) @@ -1792,22 +1792,25 @@ def capture_dimensions(): Flight data is matched on the api call name alone, so asserting on the resources a policy returns says nothing about the dimensions it asked cloudwatch for -- which is the whole of what these filters do. + + Patched on the endpoint filter: test.patch on SageMakerMetricsFilter + (below) leaves it an own get_metric_data that shadows MetricsFilter. """ dimensions = [] - get_metric_data = MetricsFilter.get_metric_data + klass = SagemakerEndpoint.filter_registry.get('metrics') + get_metric_data = klass.get_metric_data def record(self, client, params): dimensions.append(params['Dimensions']) return get_metric_data(self, client, params) - return dimensions, mock.patch.object( - MetricsFilter, 'get_metric_data', record) + return dimensions, mock.patch.object(klass, 'get_metric_data', record) ################################################################################ # # Tests that test that we actually fetch correct data by running # policies that depend on it. -# Two policies, each run against both kinds of endpoint: +# Two policies, each run against both kinds of real-time endpoint: # # test policy endpoints # ------------------------------- ---------------------- ------------------- @@ -1818,9 +1821,9 @@ def record(self, client, params): # less-than 400 # inference_component_utilization the same policy component # -# idle_component is why the idle policy needs no missing-value: an endpoint -# nothing has called reports a zero for each interval rather than nothing -# at all, whichever way it hosts its models. What a missing value is for -- +# idle_component is why the idle policy needs no missing-value: a real-time +# endpoint nothing has called reports a zero for each interval rather than +# nothing at all, whichever way it hosts its models. What a missing value is for -- # a metric with no values to compare -- is covered by the two unit tests # below it, which don't need an endpoint. # @@ -2185,6 +2188,88 @@ def zero(self, client, params): assert selected(**{'missing-value': 0}) +def endpoints_selected(test, metric, **extra): + """Which of a classic and a component endpoint an idle policy selects. + + With the endpoints asked about, the endpoints as annotated, and the + filter's log records. + """ + from c7n.resources.sagemaker import SageMakerMetricsFilter + + asked = [] + + def no_data(self, client, params): + asked.append({d['Name']: d['Value'] for d in params['Dimensions']}) + return [] + + test.patch(SageMakerMetricsFilter, 'get_metric_data', no_data) + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + f = SagemakerEndpoint.filter_registry.get('metrics')( + dict(type='metrics', name=metric, statistics='Sum', + value=0, op='lte', **extra), + policy.resource_manager) + f.endpoint_components = {'component': ['comp']} + variants = [{'VariantName': 'AllTraffic'}] + endpoints = {name: {'EndpointName': name, 'ProductionVariants': variants} + for name in ('classic', 'component')} + + selected = [r['EndpointName'] for r in f.process(list(endpoints.values()))] + return selected, asked, endpoints + + +def test_sagemaker_endpoint_metrics_unpublished_kind_ignores_missing_value( + test, caplog): + # a kind that doesn't publish the metric has no series to be missing, + # so a missing value doesn't decide it: otherwise a classic-only + # metric with a missing value reports every component endpoint idle + with caplog.at_level(logging.WARNING, logger='custodian.filters'): + selected, asked, endpoints = endpoints_selected( + test, 'InvocationsPerInstance', **{'missing-value': 0}) + assert selected == ['classic'] + # nor is cloudwatch asked about the component endpoint + assert [dims['EndpointName'] for dims in asked] == ['classic'] + assert endpoints['classic']['c7n:metrics-kind'] == 'classic' + assert endpoints['component']['c7n:metrics-kind'] == 'inference-component' + # which the author is told about, since it changed the outcome + [record] = caplog.records + assert record.levelno == logging.WARNING + assert record.getMessage() == ( + "metrics skipped 1 of 2 sagemaker-endpoint whose kind doesn't" + " publish InvocationsPerInstance: 1 inference-component") + + # and the same the other way about + caplog.clear() + with caplog.at_level(logging.WARNING, logger='custodian.filters'): + selected, asked, _ = endpoints_selected( + test, 'CPUReservation', **{'missing-value': 0}) + assert selected == ['component'] + assert [dims['EndpointName'] for dims in asked] == ['component'] + assert 'publish CPUReservation: 1 classic' in caplog.text + + # nor do real-time endpoints need guarding from an async-only metric + caplog.clear() + with caplog.at_level(logging.WARNING, logger='custodian.filters'): + selected, asked, _ = endpoints_selected( + test, 'InvocationsProcessed', **{'missing-value': 0}) + assert selected == [] + assert asked == [] + assert ('skipped 2 of 2 sagemaker-endpoint whose kind doesn\'t publish' + ' InvocationsProcessed: 1 classic, 1 inference-component') in caplog.text + + +def test_sagemaker_endpoint_metrics_unpublished_kind_quiet_without_missing_value( + test, caplog): + # without a missing value an endpoint with no series is passed over + # anyway, so skipping its kind changes nothing worth a warning + with caplog.at_level(logging.DEBUG, logger='custodian.filters'): + selected, asked, _ = endpoints_selected(test, 'InvocationsPerInstance') + assert selected == [] + assert [dims['EndpointName'] for dims in asked] == ['classic'] + assert not [r for r in caplog.records if r.levelno >= logging.WARNING] + assert "doesn't publish InvocationsPerInstance" in caplog.text + + def test_sagemaker_metrics_percentile_statistics(test): # a percentile is requested as ExtendedStatistics and comes back nested # under that key, rather than beside Timestamp like a standard statistic @@ -2285,6 +2370,19 @@ def test_sagemaker_endpoint_metrics_variant_without_components(test): 'LoadedModelCount', )) +# Metrics the async fixture's endpoints don't produce: no GPU, no +# notification configuration, and no request that errors or expires. +UNEXERCISED_ASYNC_METRICS = frozenset(( + 'GPUMemoryUtilization', + 'GPUUtilization', + 'Invocation4XXErrors', + 'Invocation5XXErrors', + 'NotificationFailures', + 'ExpiredRequests', + )) + +ASYNC_INFERENCE_CONFIG = {'OutputConfig': {'S3OutputPath': 's3://a-bucket/output'}} + def sagemaker_endpoint_metric_entries(): """Every (kind, metric) the catalogue describes for endpoints. @@ -2300,14 +2398,15 @@ def sagemaker_endpoint_metric_entries(): ] -def sagemaker_endpoint_metric_entries_published(): - """The entries these endpoints can be recorded against.""" +def sagemaker_endpoint_metric_entries_published(kinds, unexercised): + """The entries of these kinds a fixture's endpoints can be recorded against.""" return [ - entry if entry.values[1] not in UNEXERCISED_METRICS else + entry if entry.values[1] not in unexercised else pytest.param(*entry.values, id=entry.id, marks=pytest.mark.skip( reason='not produced by these endpoints')) for entry in sagemaker_endpoint_metric_entries() + if entry.values[0] in kinds ] @@ -2326,6 +2425,8 @@ def test_sagemaker_endpoint_metric_entry(test, kind, metric, published): endpoint = {'EndpointName': 'e', 'ProductionVariants': [{'VariantName': 'AllTraffic'}]} + if kind == 'async': + endpoint['AsyncInferenceConfig'] = ASYNC_INFERENCE_CONFIG f.endpoint_components = ( {'e': ['component']} if kind == 'inference-component' else {}) assert f.resource_kind(endpoint) == kind @@ -2341,6 +2442,26 @@ def test_sagemaker_endpoint_metric_entry(test, kind, metric, published): for value in dimensions.values()) +def test_sagemaker_endpoint_metrics_async_kept_apart(test): + # neither kind is asked for a metric only the other publishes + policy = test.load_policy( + {'name': 'endpoints', 'resource': 'sagemaker-endpoint'}) + klass = SagemakerEndpoint.filter_registry.get('metrics') + variants = [{'VariantName': 'AllTraffic'}] + async_endpoint = {'EndpointName': 'a', 'ProductionVariants': variants, + 'AsyncInferenceConfig': ASYNC_INFERENCE_CONFIG} + classic_endpoint = {'EndpointName': 'c', 'ProductionVariants': variants} + + def dimensions_set(metric, endpoint): + f = klass({'type': 'metrics', 'name': metric, 'value': 0}, + policy.resource_manager) + f.endpoint_components = {} + return f.get_dimensions_set(endpoint) + + assert dimensions_set('Invocations', async_endpoint) == [] + assert dimensions_set('InvocationsProcessed', classic_endpoint) == [] + + # Metrics publish a minute or so after an invocation, and one publication # window serves every case below, so the traffic is generated once for the # whole module rather than per test. @@ -2376,7 +2497,12 @@ def invoke_for_metrics(test, factory, endpoints, components): @pytest.mark.audited @pytest.mark.parametrize('kind,metric,published', - sagemaker_endpoint_metric_entries_published()) + sagemaker_endpoint_metric_entries_published( + # every kind but async, so a new kind fails + # here until it's recorded + [kind for kind in SAGEMAKER_METRICS + if kind not in (None, 'async')], + UNEXERCISED_METRICS)) @terraform('sagemaker_endpoint_metrics', scope='module') def test_sagemaker_endpoint_metric_published( test, sagemaker_endpoint_metrics, kind, metric, published): @@ -2400,7 +2526,11 @@ def test_sagemaker_endpoint_metric_published( 'ProductionVariants': [{'VariantName': variant} for variant in ENDPOINT_VARIANTS[hosting]], } + assert_metric_published(test, factory, kind, metric, resource, components) + +def assert_metric_published(test, factory, kind, metric, resource, components): + """Every dimension set the metric is filed under has a series for resource.""" p = test.load_policy( {'name': 'sagemaker-endpoint-metric', 'resource': 'sagemaker-endpoint', @@ -2424,3 +2554,94 @@ def test_sagemaker_endpoint_metric_published( assert len(series) == len(f.get_dimensions_set(resource)) assert any(points for points in series.values()), ( f"{metric} returned no data for any {kind} dimension set") + + +################################################################################ +# +# Asynchronous inference endpoints +# + + +ASYNC_INVOKED: list[str] = [] + + +def invoke_async_for_metrics(test, factory, endpoint, request): + """Give the busy async endpoint something to report, once per recording run.""" + if not test.recording or ASYNC_INVOKED: + return + + runtime = factory().client('sagemaker-runtime') + for _ in range(5): + runtime.invoke_endpoint_async( + EndpointName=endpoint, ContentType='text/csv', InputLocation=request) + time.sleep(300) + ASYNC_INVOKED.append('done') + + +@pytest.mark.audited +@terraform('sagemaker_endpoint_async_metrics', scope='module') +def test_sagemaker_endpoint_async_metrics_idle( + test, sagemaker_endpoint_async_metrics): + # idle has no InvocationsProcessed, so only a missing value matches it + busy = sagemaker_endpoint_async_metrics['aws_sagemaker_endpoint.busy.name'] + idle = sagemaker_endpoint_async_metrics['aws_sagemaker_endpoint.idle.name'] + request = sagemaker_endpoint_async_metrics.outputs['request_location']['value'] + factory = test.replay_flight_data('test_sagemaker_endpoint_async_metrics_idle') + invoke_async_for_metrics(test, factory, busy, request) + + idle_policy = { + 'name': 'sagemaker-endpoints-idle', + 'resource': 'sagemaker-endpoint', + 'filters': [ + {'type': 'value', 'key': 'EndpointName', + 'op': 'in', 'value': [busy, idle]}, + {'type': 'metrics', + 'name': 'InvocationsProcessed', + 'statistics': 'Sum', + 'days': 1, + 'period': 86400, + 'value': 0, + 'op': 'lte'}, + ], + } + # without a missing value the idle endpoint is passed over, and the busy + # one has processed requests + p = test.load_policy(idle_policy, session_factory=factory) + assert p.run() == [] + + # the same queries again, served by the same recorded responses + idle_policy['filters'][1]['missing-value'] = 0 + p = test.load_policy(idle_policy, session_factory=factory) + dimensions, capture = capture_dimensions() + with capture: + [resource] = p.run() + assert resource['EndpointName'] == idle + [points] = resource['c7n.metrics'].values() + assert points == [] + assert sorted([(d['Name'], d['Value']) for d in dims] for dims in dimensions) == [ + [('EndpointName', busy), ('VariantName', 'AllTraffic')], + [('EndpointName', idle), ('VariantName', 'AllTraffic')]] + + +@pytest.mark.audited +@pytest.mark.parametrize('kind,metric,published', + sagemaker_endpoint_metric_entries_published( + ('async',), UNEXERCISED_ASYNC_METRICS)) +@terraform('sagemaker_endpoint_async_metrics', scope='module') +def test_sagemaker_endpoint_async_metric_published( + test, sagemaker_endpoint_async_metrics, kind, metric, published): + # test_sagemaker_endpoint_metric_published, for the async endpoints + busy = sagemaker_endpoint_async_metrics['aws_sagemaker_endpoint.busy.name'] + request = sagemaker_endpoint_async_metrics.outputs['request_location']['value'] + factory = test.replay_flight_data(f"test_sagemaker_metric_async_{metric}") + invoke_async_for_metrics(test, factory, busy, request) + + resource = { + 'EndpointName': busy, + 'ProductionVariants': [{'VariantName': 'AllTraffic'}], + 'AsyncInferenceConfig': ASYNC_INFERENCE_CONFIG, + } + assert_metric_published(test, factory, kind, metric, resource, {}) + +# +################################################################################ From cfc9f9001659e87109d2e63955fd42cfa740cc02 Mon Sep 17 00:00:00 2001 From: areddyn <126821428+areddyn@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:26:08 -0400 Subject: [PATCH 55/65] aws - vpc-endpoint - skip service-details filter on endpoints without ServiceName (#11200) --- c7n/resources/vpc.py | 15 +++++++++++++-- tests/test_vpc.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py index 2875aaa0f3f..acdd049114d 100644 --- a/c7n/resources/vpc.py +++ b/c7n/resources/vpc.py @@ -2941,19 +2941,30 @@ def process(self, resources, event=None): client = local_session(self.manager.session_factory).client("ec2") current_region = self.manager.config.region + # VPC Lattice Resource/ServiceNetwork endpoints have no ServiceName, + # so they can't be matched against endpoint service details; skip them. + named_resources = [] + for r in resources: + if "ServiceName" not in r: + self.log.warning( + "Skipping VPC endpoint %s (type=%s) with no ServiceName", + r.get("VpcEndpointId"), r.get("VpcEndpointType")) + continue + named_resources.append(r) + # Group unique service names by ServiceRegion, which is set directly on # each endpoint and correctly identifies cross-region endpoints without # any string parsing. Falls back to current region for older endpoints # that pre-date the ServiceRegion field. regional_groups = {} - for r in resources: + for r in named_resources: region = r.get("ServiceRegion", current_region) regional_groups.setdefault(region, set()).add(r["ServiceName"]) service_map = self._describe_services(client, regional_groups, current_region) results = [] - for resource in resources: + for resource in named_resources: service_detail = service_map.get(resource["ServiceName"], {}) resource[self.annotation_key] = service_detail or {} diff --git a/tests/test_vpc.py b/tests/test_vpc.py index 559fc83cdfd..c61c83bc141 100644 --- a/tests/test_vpc.py +++ b/tests/test_vpc.py @@ -4832,6 +4832,27 @@ def test_endpoint_service_details_missing_service_region_uses_current(self): self.assertEqual(describe_args[1], {"us-east-1": {service_name}}) self.assertEqual(describe_args[2], "us-east-1") + def test_endpoint_service_details_skips_resources_without_service_name(self): + # VPC Lattice Resource/ServiceNetwork endpoints omit ServiceName + # entirely; they must be skipped rather than raising a KeyError. + service_filter = self.get_filter() + service_filter.manager.session_factory = MagicMock() + service_filter.manager.session_factory.region = "us-east-1" + service_name = "com.amazonaws.us-east-1.s3" + service_filter._describe_services = MagicMock(return_value={ + service_name: {"VpcEndpointPolicySupported": True} + }) + + resources = service_filter.process([ + {"VpcEndpointId": "vpce-lattice-1", "VpcEndpointType": "Resource"}, + {"ServiceName": service_name}, + ]) + + self.assertEqual(len(resources), 1) + self.assertEqual(resources[0]["ServiceName"], service_name) + describe_args = service_filter._describe_services.call_args.args + self.assertEqual(describe_args[1], {"us-east-1": {service_name}}) + def test_endpoint_service_details_policy_supported(self): session_factory = self.replay_flight_data( "test_vpc_endpoint_service_details_filter" From eb662ddc66db2de16a1574a33873d7fdbc7dbe54 Mon Sep 17 00:00:00 2001 From: Srinivas Chippagiri Date: Mon, 19 Oct 2026 19:32:20 -0700 Subject: [PATCH 56/65] aws - apigw-domain-name - use API-provided ARN for private custom domains (#11226) Signed-off-by: Srinivas Chippagiri --- c7n/resources/apigw.py | 10 ++++++ .../apigateway.GetDomainNames_1.json | 33 +++++++++++++++++++ tests/test_apigw.py | 20 +++++++++++ 3 files changed, 63 insertions(+) create mode 100644 tests/data/placebo/test_apigw_domain_name_private_arn/apigateway.GetDomainNames_1.json diff --git a/c7n/resources/apigw.py b/c7n/resources/apigw.py index 38f5d2364bc..15867934855 100644 --- a/c7n/resources/apigw.py +++ b/c7n/resources/apigw.py @@ -1116,6 +1116,16 @@ def generate_arn(self): resource_type=self.resource_type.arn_type) return self._generate_arn + def get_arns(self, resources): + # Private custom domain names have an account-scoped ARN that + # includes the domain name id, e.g. + # arn:aws:apigateway:us-east-1:111122223333:/domainnames/example.com+abcd1234 + # so prefer the ARN returned by the API when it is present. + return [ + r.get('domainNameArn') or self.generate_arn(r['domainName']) + for r in resources + ] + @CustomDomainName.action_registry.register('update-security') class DomainNameRemediateTls(BaseAction): diff --git a/tests/data/placebo/test_apigw_domain_name_private_arn/apigateway.GetDomainNames_1.json b/tests/data/placebo/test_apigw_domain_name_private_arn/apigateway.GetDomainNames_1.json new file mode 100644 index 00000000000..858da4d4b17 --- /dev/null +++ b/tests/data/placebo/test_apigw_domain_name_private_arn/apigateway.GetDomainNames_1.json @@ -0,0 +1,33 @@ +{ + "status_code": 200, + "data": { + "ResponseMetadata": {}, + "items": [ + { + "domainName": "public.example.com", + "regionalDomainName": "d-o34butr3q7.execute-api.us-east-1.amazonaws.com", + "regionalHostedZoneId": "Z1UJRXOUMOOFQ8", + "endpointConfiguration": { + "types": [ + "REGIONAL" + ] + }, + "domainNameStatus": "AVAILABLE", + "securityPolicy": "TLS_1_2" + }, + { + "domainName": "private.example.com", + "domainNameId": "abcd1234", + "domainNameArn": "arn:aws:apigateway:us-east-1:644160558196:/domainnames/private.example.com+abcd1234", + "certificateArn": "arn:aws:acm:us-east-1:644160558196:certificate/9d7fca5a-5c58-4d43-b1df-7c7e31c2d47f", + "endpointConfiguration": { + "types": [ + "PRIVATE" + ] + }, + "domainNameStatus": "AVAILABLE", + "securityPolicy": "TLS_1_2" + } + ] + } +} diff --git a/tests/test_apigw.py b/tests/test_apigw.py index 6e781290916..3afdc3a1a84 100644 --- a/tests/test_apigw.py +++ b/tests/test_apigw.py @@ -1158,6 +1158,26 @@ def test_arn_format(self): "arn:aws:apigateway:us-east-1::/domainnames/bad.example.com" ) + def test_private_domain_name_arn(self): + factory = self.replay_flight_data("test_apigw_domain_name_private_arn") + p = self.load_policy( + { + "name": "apigw-domain-name-private-arn", + "resource": "apigw-domain-name", + }, + session_factory=factory, + ) + resources = p.run() + self.assertEqual(len(resources), 2) + self.assertEqual( + p.resource_manager.get_arns(resources), + [ + "arn:aws:apigateway:us-east-1::/domainnames/public.example.com", + "arn:aws:apigateway:us-east-1:644160558196:" + "/domainnames/private.example.com+abcd1234", + ], + ) + class TestResourcePolicy(BaseTest): def test_rest_api_default_resource_policy(self): From b377a9d68b86442dd49e31b8d16de1c388f8e9ec Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Tue, 20 Oct 2026 08:03:16 +0530 Subject: [PATCH 57/65] core - resolver - skip blank rows in value_from csv2dict (#11227) --- c7n/resolver.py | 4 +++- tests/test_resolver.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/c7n/resolver.py b/c7n/resolver.py index c3c370c50c8..035f5e24c83 100644 --- a/c7n/resolver.py +++ b/c7n/resolver.py @@ -288,7 +288,9 @@ def _get_values(self): elif format == 'csv' or format == 'csv2dict': data = csv.reader(io.StringIO(contents)) if format == 'csv2dict': - data = {x[0]: list(x[1:]) for x in zip(*data)} + # csv.reader returns a blank line as an empty row, and zip() stops at + # its shortest input, so drop empty rows before transposing. + data = {x[0]: list(x[1:]) for x in zip(*(row for row in data if row))} if 'expr' in self.data: return self._get_resource_values(data) else: diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 058bf350be5..97447e50c12 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -497,6 +497,41 @@ def test_csv_no_expr_using_dict(self): os.remove("test_dict.csv") self.assertEqual(values.get_values(), {"0", "1", "2", "3", "4"}) + def test_csv_expr_using_dict_blank_rows(self): + values = self.get_values_from( + {"url": "sun.csv", "expr": "account_id", "format": "csv2dict"}, + "account_id,reason\n111,special\n222,\n\n444,\n", + ) + self.assertEqual(values.get_values(), {"111", "222", "444"}) + + def test_csv_expr_using_dict_trailing_blank_rows(self): + values = self.get_values_from( + {"url": "sun.csv", "expr": "reason", "format": "csv2dict"}, + "account_id,reason\r\n111,special\r\n222,\r\n\r\n", + ) + self.assertEqual(values.get_values(), {"special", ""}) + + def test_csv_expr_using_dict_blank_rows_index(self): + values = self.get_values_from( + {"url": "sun.csv", "expr": "account_id[2]", "format": "csv2dict"}, + "\naccount_id,reason\n111,special\n\n222,\n444,\n", + ) + self.assertEqual(values.get_values(), "444") + + def test_csv_no_expr_using_dict_blank_rows(self): + values = self.get_values_from( + {"url": "sun.csv", "format": "csv2dict"}, + "account_id,reason\n111,special\n\n222,\n", + ) + self.assertEqual(values.get_values(), {"111", "special", "222", ""}) + + def test_csv_expr_using_dict_short_rows_kept(self): + values = self.get_values_from( + {"url": "sun.csv", "expr": "account_id", "format": "csv2dict"}, + "account_id,reason\n111,special\n222\n", + ) + self.assertEqual(values.get_values(), {"111", "222"}) + def test_csv_column(self): with open("test_column.csv", "w") as out: writer = csv.writer(out) From c7401e1e53cbc645411b092074b53301d681da8b Mon Sep 17 00:00:00 2001 From: Liam Callahan Date: Mon, 19 Oct 2026 22:36:32 -0400 Subject: [PATCH 58/65] azure - firewall - stop set-firewall-rules corrupting rules across resources (#11144) --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- tools/c7n_azure/c7n_azure/actions/firewall.py | 17 ++-- .../tests_azure/test_firewall_actions.py | 92 ++++++++++++++++++- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/tools/c7n_azure/c7n_azure/actions/firewall.py b/tools/c7n_azure/c7n_azure/actions/firewall.py index abe4c8a9299..2ba6684355e 100644 --- a/tools/c7n_azure/c7n_azure/actions/firewall.py +++ b/tools/c7n_azure/c7n_azure/actions/firewall.py @@ -36,16 +36,21 @@ def _process_resource(self, resource): pass def _build_bypass_rules(self, existing_bypass, new_rules): + # 'None' is an exclusive value meaning "bypass nothing", not a set member. + # Azure returns the string comma-space separated, e.g. "Logging, Metrics". + rules = set(new_rules) if self.append: - without_duplicates = [r for r in existing_bypass if r not in new_rules] - new_rules.extend(without_duplicates) - return ','.join(new_rules or ['None']) + existing = {b.strip() for b in existing_bypass} + rules.update(existing - {'', 'None'}) + return ','.join((sorted(rules) if self.append else new_rules) or ['None']) def _build_vnet_rules(self, existing_vnet, new_rules): + rules = list(new_rules) if self.append: - without_duplicates = [r for r in existing_vnet if r not in new_rules] - new_rules.extend(without_duplicates) - return new_rules + for rule in existing_vnet: + if rule not in rules: + rules.append(rule) + return rules def _build_ip_rules(self, existing_ip, new_rules): rules = [] diff --git a/tools/c7n_azure/tests_azure/test_firewall_actions.py b/tools/c7n_azure/tests_azure/test_firewall_actions.py index bf7f8eeede5..12043124ec4 100644 --- a/tools/c7n_azure/tests_azure/test_firewall_actions.py +++ b/tools/c7n_azure/tests_azure/test_firewall_actions.py @@ -1,5 +1,7 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from unittest.mock import Mock + from .azure_common import BaseTest from c7n_azure.resources.storage import StorageSetFirewallAction import pytest @@ -20,7 +22,64 @@ def test_build_bypass_rules(self): action.append = True rules = action._build_bypass_rules(['Hello', 'World'], data['bypass-rules']) - self.assertEqual('Logging,Metrics,Hello,World', rules) + self.assertEqual('Hello,Logging,Metrics,World', rules) + + def test_build_bypass_rules_drops_the_none_sentinel(self): + action = self._bypass_action(['AzureServices']) + + # 'None' means "bypass nothing"; merging it as a member is not a valid value. + assert action._build_bypass_rules( + 'None'.split(','), action.data['bypass-rules']) == 'AzureServices' + + def test_build_bypass_rules_ignores_an_absent_bypass_value(self): + action = self._bypass_action(['AzureServices']) + + # storage.py falls back to '' when the account has no bypass key. + assert action._build_bypass_rules( + ''.split(','), action.data['bypass-rules']) == 'AzureServices' + + def test_build_bypass_rules_matches_existing_values_with_whitespace(self): + action = self._bypass_action(['Metrics']) + + # Azure returns the string comma-space separated. + assert action._build_bypass_rules( + 'Logging, Metrics'.split(','), action.data['bypass-rules']) == 'Logging,Metrics' + + def test_build_bypass_rules_does_not_carry_over_between_resources(self): + action = self._bypass_action(['AzureServices']) + + first = action._build_bypass_rules('Logging'.split(','), action.data['bypass-rules']) + second = action._build_bypass_rules('Metrics'.split(','), action.data['bypass-rules']) + + assert first == 'AzureServices,Logging' + assert second == 'AzureServices,Metrics' + assert action.data['bypass-rules'] == ['AzureServices'] + + def test_build_bypass_rules_empty_result_is_the_none_sentinel(self): + action = self._bypass_action([], append=False) + + assert action._build_bypass_rules('Logging'.split(','), action.data['bypass-rules']) \ + == 'None' + + def test_build_vnet_rules_does_not_carry_over_between_resources(self): + action = StorageSetFirewallAction( + {'type': 'set-firewall-rules', 'virtual-network-rules': []}) + action.append = True + + first = action._build_vnet_rules(['id1'], action.data['virtual-network-rules']) + second = action._build_vnet_rules(['id2'], action.data['virtual-network-rules']) + + assert first == ['id1'] + assert second == ['id2'] + assert action.data['virtual-network-rules'] == [] + + def test_build_vnet_rules_skips_rules_already_in_the_policy(self): + action = StorageSetFirewallAction( + {'type': 'set-firewall-rules', 'virtual-network-rules': ['id1']}) + action.append = True + + assert action._build_vnet_rules( + ['id1', 'id2'], action.data['virtual-network-rules']) == ['id1', 'id2'] def test_build_vnet_rules(self): data = { @@ -68,3 +127,34 @@ def test_build_ip_rules_alias(self): rules = action._build_ip_rules(['1.1.1.1', '8.0.0.0/12'], data['ip-rules']) self.assertTrue({'6.0.0.0/16', '1.1.1.1', '8.0.0.0/12'} <= set(rules)) self.assertEqual(6, len(rules)) + + def test_process_resource_keeps_each_account_to_its_own_rules(self): + action = StorageSetFirewallAction({ + 'type': 'set-firewall-rules', + 'default-action': 'Deny', + 'bypass-rules': ['AzureServices'], + 'virtual-network-rules': [], + }) + action.append = True + action.client = Mock() + + for name, acls in [ + ('hr', {'bypass': 'None', 'virtualNetworkRules': [{'id': '/subnets/hr'}]}), + ('public', {'bypass': 'None', 'virtualNetworkRules': []}), + ]: + action._process_resource( + {'name': name, 'resourceGroup': 'rg', 'properties': {'networkAcls': acls}}) + + sent = [c[0][2].network_rule_set + for c in action.client.storage_accounts.update.call_args_list] + + assert [r.bypass for r in sent] == ['AzureServices', 'AzureServices'] + # The account with no VNet access must not inherit the one processed before it. + assert [[v.virtual_network_resource_id for v in r.virtual_network_rules] + for r in sent] == [['/subnets/hr'], []] + + def _bypass_action(self, bypass_rules, append=True): + action = StorageSetFirewallAction( + {'type': 'set-firewall-rules', 'bypass-rules': bypass_rules}) + action.append = append + return action From aa8512a8674cda9df188d0ed1b870281dbf5c256 Mon Sep 17 00:00:00 2001 From: Daniel Lindsley Date: Mon, 19 Oct 2026 21:42:11 -0500 Subject: [PATCH 59/65] azure - machine-learning-job - avoid crashing on recently deleted parents (#11169) --- .../resources/machine_learning_job.py | 1 + .../test_machine_learning_job.py | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py index 8e0d5052817..ff56faafaae 100644 --- a/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py +++ b/tools/c7n_azure/c7n_azure/resources/machine_learning_job.py @@ -39,6 +39,7 @@ class MachineLearningJob(ChildArmResourceManager): class resource_type(ChildArmResourceManager.resource_type): doc_groups = ['AI + Machine Learning'] service = 'azure.mgmt.machinelearningservices' + raise_on_exception = False client = 'MachineLearningServicesMgmtClient' enum_spec = ('jobs', 'list', None) parent_manager_name = 'machine-learning-workspace' diff --git a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py index 98141f2efda..d3d88d1a0b3 100644 --- a/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py +++ b/tools/c7n_azure/tests_azure/tests_resources/test_machine_learning_job.py @@ -1,6 +1,10 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from unittest.mock import Mock + +from azure.core.exceptions import ResourceNotFoundError + from c7n.utils import local_session from c7n_azure.query import _serialize from c7n_azure.resources.machine_learning_job import ( @@ -167,6 +171,54 @@ def test_machine_learning_job_archive(self): ) self.assertTrue(job.properties.is_archived) + def test_machine_learning_job_continues_after_failed_workspace(self): + # When one workspace raises ResourceNotFoundError during child enumeration + # (e.g. a ghost entry in Azure Resource Graph for a deleted workspace), + # the run should continue and return jobs from healthy workspaces. + ghost_parent = { + 'id': '/subscriptions/sub/resourceGroups/rg-ghost/providers/' + 'Microsoft.MachineLearningServices/workspaces/ghost-ws', + 'name': 'ghost-ws', + 'resourceGroup': 'rg-ghost', + } + good_parent = { + 'id': '/subscriptions/sub/resourceGroups/rg/providers/' + 'Microsoft.MachineLearningServices/workspaces/good-ws', + 'name': 'good-ws', + 'resourceGroup': 'rg', + } + good_job = { + 'id': '/subscriptions/sub/resourceGroups/rg/providers/' + 'Microsoft.MachineLearningServices/workspaces/good-ws/jobs/job1', + 'name': 'job1', + 'resourceGroup': 'rg', + 'type': 'Microsoft.MachineLearningServices/workspaces/jobs', + 'properties': {'jobType': 'Command'}, + } + + parent_manager = Mock() + parent_manager.resource_type.id = 'id' + parent_manager.resources.return_value = [ghost_parent, good_parent] + + p = self.load_policy({ + 'name': 'find-all-machine-learning-jobs', + 'resource': 'azure.machine-learning-job', + }) + manager = p.resource_manager + manager.get_parent_manager = Mock(return_value=parent_manager) + + def enumerate_side_effect(parent, type_info, **kwargs): + if parent['name'] == 'ghost-ws': + raise ResourceNotFoundError('ParentResourceNotFound') + return [good_job] + + manager.enumerate_resources = Mock(side_effect=enumerate_side_effect) + + resources = manager.resources() + + self.assertEqual(1, len(resources)) + self.assertEqual('job1', resources[0]['name']) + @arm_template('machine-learning-job-archive.json') @cassette_name('machine-learning-job-archive-skip') def test_machine_learning_job_archive_skips_archived_job(self): From 42852392a2653da8e551195e4686db384edec31e Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Tue, 20 Oct 2026 05:45:07 +0300 Subject: [PATCH 60/65] gcp - set-iam-policy - add auditConfig support for Data Access audit logs (#10650) --------- Co-authored-by: Daniel Lindsley Co-authored-by: Kody Mullins --- tools/c7n_gcp/c7n_gcp/actions/iampolicy.py | 370 ++++++++++++++++-- ...emanager.googleapis.com-v1-projects_1.json | 36 ++ ...ects-stkl-kody-mullins-getIamPolicy_1.json | 116 ++++++ ...ects-stkl-kody-mullins-getIamPolicy_2.json | 116 ++++++ ...ects-stkl-kody-mullins-getIamPolicy_3.json | 116 ++++++ ...ects-stkl-kody-mullins-setIamPolicy_1.json | 116 ++++++ ...emanager.googleapis.com-v1-projects_1.json | 36 ++ ...ects-stkl-kody-mullins-getIamPolicy_1.json | 116 ++++++ ...ects-stkl-kody-mullins-getIamPolicy_2.json | 116 ++++++ ...ects-stkl-kody-mullins-getIamPolicy_3.json | 110 ++++++ ...ects-stkl-kody-mullins-setIamPolicy_1.json | 110 ++++++ ...emanager.googleapis.com-v1-projects_1.json | 36 ++ ...emanager.googleapis.com-v1-projects_2.json | 36 ++ ...emanager.googleapis.com-v1-projects_3.json | 36 ++ ...ects-stkl-kody-mullins-getIamPolicy_1.json | 86 ++++ ...ects-stkl-kody-mullins-getIamPolicy_2.json | 99 +++++ ...ects-stkl-kody-mullins-getIamPolicy_3.json | 99 +++++ ...ects-stkl-kody-mullins-getIamPolicy_4.json | 96 +++++ ...ects-stkl-kody-mullins-getIamPolicy_5.json | 96 +++++ ...ects-stkl-kody-mullins-getIamPolicy_6.json | 105 +++++ ...ects-stkl-kody-mullins-setIamPolicy_1.json | 99 +++++ ...ects-stkl-kody-mullins-setIamPolicy_2.json | 96 +++++ ...ects-stkl-kody-mullins-setIamPolicy_3.json | 105 +++++ .../gcp_project_iam_audit_wildcard/main.tf | 36 ++ .../tf_resources.json | 86 ++++ .../main.tf | 25 ++ .../tf_resources.json | 56 +++ tools/c7n_gcp/tests/test_resourcemanager.py | 258 ++++++++++++ 28 files changed, 2781 insertions(+), 27 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_3.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_4.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_5.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_6.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_2.json create mode 100644 tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_3.json create mode 100644 tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/tf_resources.json create mode 100644 tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/actions/iampolicy.py b/tools/c7n_gcp/c7n_gcp/actions/iampolicy.py index bfd48e2964a..7135f1319ff 100644 --- a/tools/c7n_gcp/c7n_gcp/actions/iampolicy.py +++ b/tools/c7n_gcp/c7n_gcp/actions/iampolicy.py @@ -1,24 +1,91 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 +from c7n.exceptions import PolicyValidationError from c7n.utils import local_session, type_schema from c7n_gcp.actions import MethodAction from c7n_gcp.filters.iampolicy import IamPolicyFilter +_AUDIT_LOG_CONFIG_SCHEMA = { + 'type': 'object', + 'required': ['log-type'], + 'additionalProperties': False, + 'properties': { + 'log-type': { + 'type': 'string', + 'enum': ['ADMIN_READ', 'DATA_READ', 'DATA_WRITE'], + }, + 'exempted-members': { + 'type': 'array', + 'items': {'type': 'string'}, + 'minItems': 1, + }, + }, +} + +_AUDIT_CONFIG_SCHEMA = { + 'type': 'array', + 'minItems': 1, + 'items': { + 'type': 'object', + 'required': ['service', 'audit-log-configs'], + 'additionalProperties': False, + 'properties': { + 'service': {'type': 'string'}, + 'audit-log-configs': { + 'type': 'array', + 'minItems': 1, + 'items': _AUDIT_LOG_CONFIG_SCHEMA, + }, + }, + }, +} + +# Removals also accept exempted-members: '*' to clear every exempted member. +_REMOVE_AUDIT_LOG_CONFIG_SCHEMA = { + **_AUDIT_LOG_CONFIG_SCHEMA, + 'properties': { + **_AUDIT_LOG_CONFIG_SCHEMA['properties'], + 'exempted-members': {'oneOf': [ + _AUDIT_LOG_CONFIG_SCHEMA['properties']['exempted-members'], + {'enum': ['*']}, + ]}, + }, +} + +_REMOVE_AUDIT_CONFIG_SCHEMA = { + 'type': 'array', + 'minItems': 1, + 'items': { + 'type': 'object', + 'required': ['service', 'audit-log-configs'], + 'additionalProperties': False, + 'properties': { + 'service': {'type': 'string'}, + 'audit-log-configs': { + 'type': 'array', + 'minItems': 1, + 'items': _REMOVE_AUDIT_LOG_CONFIG_SCHEMA, + }, + }, + }, +} + class SetIamPolicy(MethodAction): - """ Sets IAM policy. It works with bindings only. + """ Sets IAM policy. Supports both role bindings and auditConfigs (Data Access audit logs). The action supports two lists for modifying the existing IAM policy: `add-bindings` and - `remove-bindings`. The `add-bindings` records are merged with the existing bindings, hereby - no changes are made if all the required bindings are already present in the applicable - resource. The `remove-bindings` records are used to filter out the existing bindings, - so the action will take no effect if there are no matches. For more information, + `remove-bindings`. The `add-bindings` records are merged with the existing bindings, + hereby no changes are made if all the required bindings are already present in the + applicable resource. The `remove-bindings` records are used to filter out the existing + bindings, so the action will take no effect if there are no matches. For more information, please refer to the `_add_bindings` and `_remove_bindings` methods respectively. - Considering a record added both to the `add-bindings` and `remove-bindings` lists, which - though is not a recommended thing to do in general, the latter is designed to be a more - restrictive one, so the record will be removed from the existing IAM bindings in the end. + Considering a record added both to the `add-bindings` and `remove-bindings` lists, + which though is not a recommended thing to do in general, the latter is designed to be a + more restrictive one, so the record will be removed from the existing IAM bindings in the + end. There following member types are available to work with: - allUsers, @@ -28,8 +95,18 @@ class SetIamPolicy(MethodAction): - domain, - serviceAccount. - Note the `resource` field in the example that could be changed to another resource that has - both `setIamPolicy` and `getIamPolicy` methods (such as gcp.spanner-database-instance). + The `add-audit-configs` and `remove-audit-configs` keys allow enabling, disabling, + or adjusting Data Access audit logs (`ADMIN_READ`, `DATA_READ`, `DATA_WRITE`) and + their `exemptedMembers`. When removing, if no `exempted-members` are specified the + entire `log-type` entry is removed; if `exempted-members` are specified only those + members are pruned. A service entry is dropped entirely when all its log-types are removed. + In `remove-audit-configs` only, `exempted-members: '*'` clears every exempted member + while keeping the log type enabled, and `service: '*'` applies the entry to every + service in the policy. `service: '*'` requires `exempted-members` on each log type, so + it can never disable logging. + + Note the `resource` field in the examples that could be changed to another resource that + has both `setIamPolicy` and `getIamPolicy` methods (such as gcp.spanner-database-instance). Example using exact values: @@ -78,6 +155,61 @@ class SetIamPolicy(MethodAction): actions: - type: set-iam-policy remove-bindings: matched + + Example enabling Data Access audit logs and adding an exempted member: + + .. code-block:: yaml + + policies: + - name: enable-data-access-audit-logs + resource: gcp.project + actions: + - type: set-iam-policy + add-audit-configs: + - service: allServices + audit-log-configs: + - log-type: DATA_READ + - log-type: DATA_WRITE + exempted-members: + - user:admin@example.com + + Example removing a specific audit log type: + + .. code-block:: yaml + + policies: + - name: remove-data-write-audit-log + resource: gcp.project + actions: + - type: set-iam-policy + remove-audit-configs: + - service: allServices + audit-log-configs: + - log-type: DATA_WRITE + + Example clearing every exempted member on every service while keeping logging on: + + .. code-block:: yaml + + policies: + - name: remove-data-access-audit-exemptions + resource: gcp.project + filters: + - type: iam-policy + doc: + key: "auditConfigs[].auditLogConfigs[].exemptedMembers[]" + value: not-null + actions: + - type: set-iam-policy + remove-audit-configs: + - service: '*' + audit-log-configs: + - log-type: ADMIN_READ + exempted-members: '*' + - log-type: DATA_READ + exempted-members: '*' + - log-type: DATA_WRITE + exempted-members: '*' """ schema = type_schema('set-iam-policy', **{ @@ -107,52 +239,106 @@ class SetIamPolicy(MethodAction): {'enum': ['*']}]}}} ] }, + 'add-audit-configs': _AUDIT_CONFIG_SCHEMA, + 'remove-audit-configs': _REMOVE_AUDIT_CONFIG_SCHEMA, }) method_spec = {'op': 'setIamPolicy'} schema_alias = True + def validate(self): + super().validate() + for spec in self.data.get('add-audit-configs', []): + if spec['service'] == '*': + raise PolicyValidationError( + f"{self.type}: add-audit-configs does not support service '*'") + for spec in self.data.get('remove-audit-configs', []): + # Dropping whole log types from every service would wipe all audit logging. + if spec['service'] == '*' and any( + 'exempted-members' not in lc for lc in spec['audit-log-configs']): + raise PolicyValidationError( + f"{self.type}: remove-audit-configs with service '*' requires " + "exempted-members on every audit-log-config") + return self + def get_resource_params(self, model, resource): """ - Collects `existing_bindings` with the `_get_existing_bindings` method, `add_bindings` and - `remove_bindings` from a policy, applies `_add_bindings` to the `existing_bindings`, then - removes bindings either via `_remove_matched_bindings` (when ``remove-bindings: matched``) - or `_remove_bindings` (when an explicit list is provided), and finally sets the resulting - list at the 'bindings' key if there is at least a single record there, or assigns an empty - object to the 'policy' key in order to avoid errors produced by the API. + Fetches the full existing IAM policy once via `_get_existing_policy`, then: + - merges/removes bindings via `_add_bindings` / `_remove_bindings` / + `_remove_matched_bindings`, + - merges/removes auditConfigs via `_add_audit_configs` / `_remove_audit_configs`, + - round-trips the policy `etag` to prevent lost-update races and `version` to + preserve IAM Conditions support (version 3 policies), + - assembles the final `setIamPolicy` body. :param model: the parameters that are defined in a resource manager :param resource: the resource the action is applied to """ params = self._verb_arguments(resource) - existing_bindings = self._get_existing_bindings(model, resource) + policy = self._get_existing_policy(model, resource) + + existing_bindings = policy.get('bindings', []) + existing_audit_configs = policy.get('auditConfigs', []) + add_bindings = self.data.get('add-bindings', []) remove_bindings = self.data.get('remove-bindings', []) - bindings_to_set = self._add_bindings(existing_bindings, add_bindings) + add_audit_configs = self.data.get('add-audit-configs', []) + remove_audit_configs = self.data.get('remove-audit-configs', []) + bindings_to_set = self._add_bindings(existing_bindings, add_bindings) if remove_bindings == 'matched': matched_pairs = resource.get(IamPolicyFilter.annotation_key, []) bindings_to_set = self._remove_matched_bindings(bindings_to_set, matched_pairs) else: bindings_to_set = self._remove_bindings(bindings_to_set, remove_bindings) - params['body'] = { - 'policy': {'bindings': bindings_to_set} if len(bindings_to_set) > 0 else {}} + audit_configs_to_set = self._add_audit_configs(existing_audit_configs, add_audit_configs) + audit_configs_to_set = self._remove_audit_configs( + audit_configs_to_set, remove_audit_configs) + + changes_bindings = bool(add_bindings or remove_bindings) + changes_audit_configs = bool(add_audit_configs or remove_audit_configs) + + new_policy = {} + # Audit-only updates leave bindings out of both the body and the update mask, so + # conditional bindings (which a non-v3 getIamPolicy returns without their + # conditions) are never written back. + if bindings_to_set and (changes_bindings or not changes_audit_configs): + new_policy['bindings'] = bindings_to_set + if audit_configs_to_set: + new_policy['auditConfigs'] = audit_configs_to_set + if 'etag' in policy: + new_policy['etag'] = policy['etag'] + if 'version' in policy: + new_policy['version'] = policy['version'] + + params['body'] = {'policy': new_policy} + if changes_audit_configs: + mask = ['bindings', 'etag', 'auditConfigs'] if changes_bindings else [ + 'etag', 'auditConfigs'] + params['body']['updateMask'] = ','.join(mask) return params - def _get_existing_bindings(self, model, resource): + def _get_existing_policy(self, model, resource): """ - Calls the `getIamPolicy` method on the resource the action is applied to and returns - either a list of existing bindings or an empty one if there is no 'bindings' key. + Calls the `getIamPolicy` method and returns the full policy dict. - :param model: the same as in `get_resource_params` (needed to take `component` from) - :param resource: the same as in `get_resource_params` (passed into `_verb_arguments`) + :param model: the same as in `get_resource_params` + :param resource: the same as in `get_resource_params` """ - existing_bindings = local_session(self.manager.session_factory).client( + return local_session(self.manager.session_factory).client( self.manager.resource_type.service, self.manager.resource_type.version, model.component).execute_query( 'getIamPolicy', verb_arguments=self._verb_arguments(resource)) - return existing_bindings['bindings'] if 'bindings' in existing_bindings else [] + + def _get_existing_bindings(self, model, resource): + """ + Returns the existing `bindings` list from the current IAM policy. + + :param model: the same as in `get_resource_params` + :param resource: the same as in `get_resource_params` + """ + return self._get_existing_policy(model, resource).get('bindings', []) def _verb_arguments(self, resource): """ @@ -265,6 +451,136 @@ def _remove_matched_bindings(self, existing_bindings, matched_pairs): bindings.append({**binding, 'members': updated_members}) return bindings + def _add_audit_configs(self, existing, to_add): + """Merge new auditConfig entries into the existing list. + + For each service in `to_add`: + - If the service is not present in `existing`, it is added wholesale. + - If the service already exists, its `auditLogConfigs` are merged by `logType`: + - New log types are appended. + - Existing log types have their `exemptedMembers` unioned (no duplicates). + + This method is idempotent: running it again with the same `to_add` produces no change. + + :param existing: list of auditConfig dicts from the current IAM policy + :param to_add: list of auditConfig specs from the policy YAML + """ + by_service = {e['service']: e for e in existing} + + for spec in to_add: + service = spec['service'] + new_log_configs = spec.get('audit-log-configs', []) + + if service not in by_service: + by_service[service] = { + 'service': service, + 'auditLogConfigs': [self._spec_to_log_config(lc) for lc in new_log_configs], + } + else: + existing_entry = by_service[service] + existing_by_type = { + lc['logType']: lc + for lc in existing_entry.get('auditLogConfigs', []) + } + for lc_spec in new_log_configs: + log_type = lc_spec['log-type'] + new_members = lc_spec.get('exempted-members', []) + if log_type not in existing_by_type: + existing_by_type[log_type] = self._spec_to_log_config(lc_spec) + elif new_members: + existing_members = existing_by_type[log_type].get('exemptedMembers', []) + merged = list(dict.fromkeys(existing_members + new_members)) + existing_by_type[log_type] = {'logType': log_type, + 'exemptedMembers': merged} + existing_entry['auditLogConfigs'] = list(existing_by_type.values()) + + return list(by_service.values()) + + def _remove_audit_configs(self, existing, to_remove): + """Prune auditConfig entries from the existing list. + + For each service in `to_remove` (`'*'` matches every service in `existing`): + - If the service is not present in `existing`, it is skipped (no-op). + - For each log type in the removal spec: + - If no `exempted-members` are specified, the entire log type entry is removed. + - If `exempted-members` is `'*'`, every exempted member is removed; the log type + itself is kept. + - If `exempted-members` are specified, only those members are removed from the + `exemptedMembers` list; the log type itself is kept (even if exemptedMembers + becomes empty, the logType remains enabled). + - If removing log types empties a service's `auditLogConfigs`, that service entry + is dropped entirely. + + :param existing: list of auditConfig dicts from the current IAM policy + :param to_remove: list of auditConfig specs from the policy YAML + """ + removals = {} + for spec in to_remove: + types = removals.setdefault(spec['service'], {}) + for lc_spec in spec.get('audit-log-configs', []): + self._merge_audit_log_removal( + types, lc_spec['log-type'], lc_spec.get('exempted-members', [])) + wildcard_types = removals.pop('*', {}) + result = [] + + for entry in existing: + service = entry['service'] + if service not in removals and not wildcard_types: + result.append(entry) + continue + + types_to_remove = dict(removals.get(service, {})) + for log_type, members in wildcard_types.items(): + self._merge_audit_log_removal(types_to_remove, log_type, members) + + filtered_log_configs = [] + for lc in entry.get('auditLogConfigs', []): + log_type = lc['logType'] + if log_type not in types_to_remove: + filtered_log_configs.append(lc) + continue + members_to_remove = types_to_remove[log_type] + if not members_to_remove: + continue + if members_to_remove == '*': + filtered_log_configs.append({'logType': log_type}) + continue + remaining = [m for m in lc.get('exemptedMembers', []) if m not in members_to_remove] + pruned = {'logType': log_type} + if remaining: + pruned['exemptedMembers'] = remaining + filtered_log_configs.append(pruned) + + if filtered_log_configs: + result.append({'service': service, 'auditLogConfigs': filtered_log_configs}) + + return result + + def _merge_audit_log_removal(self, types, log_type, members): + """Accumulate a log-type removal so repeated specs don't overwrite each other. + + An empty member list (remove the whole log type) wins over `'*'`, which wins over + an explicit member list. + + :param types: dict of log type to pending removal, updated in place + :param log_type: the log type the removal applies to + :param members: `[]`, `'*'`, or a list of exempted members to remove + """ + current = types.get(log_type) + if current == [] or not members: + types[log_type] = [] + elif current == '*' or members == '*': + types[log_type] = '*' + else: + types[log_type] = (current or []) + members + + def _spec_to_log_config(self, spec): + """Convert a policy YAML log-config spec to a GCP auditLogConfig dict.""" + lc = {'logType': spec['log-type']} + if 'exempted-members' in spec: + lc['exemptedMembers'] = list(spec['exempted-members']) + return lc + def _get_roles_to_bindings_dict(self, bindings_list): """ Converts a given list to a dictionary, values under the 'role' key in elements of whose diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..406c6d768e1 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:29:22 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=223", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "27424", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "82151825516", + "projectId": "stkl-kody-mullins", + "lifecycleState": "ACTIVE", + "name": "stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-29T16:15:10.136Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json new file mode 100644 index 00000000000..613313ce67d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:29:23 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=121", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2590", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1UzGW+Y=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + }, + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json new file mode 100644 index 00000000000..e37cf99397a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:29:23 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=169", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2590", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1UzGW+Y=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + }, + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json new file mode 100644 index 00000000000..fb7f8cc4d74 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:29:23 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=187", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2431", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1U0g75w=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE" + }, + { + "logType": "ADMIN_READ" + }, + { + "logType": "DATA_READ" + } + ] + }, + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json new file mode 100644 index 00000000000..2bbdd389eb1 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-add-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:29:23 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=436", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2431", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1U0g75w=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE" + }, + { + "logType": "ADMIN_READ" + }, + { + "logType": "DATA_READ" + } + ] + }, + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..4760fe369bc --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:28:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=199", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "27424", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "82151825516", + "projectId": "stkl-kody-mullins", + "lifecycleState": "ACTIVE", + "name": "stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-29T16:15:10.136Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json new file mode 100644 index 00000000000..ed00e8f5043 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:28:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=145", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2590", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1UoZ1/U=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + }, + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json new file mode 100644 index 00000000000..7207a46484d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json @@ -0,0 +1,116 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:28:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=168", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2590", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1UoZ1/U=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + }, + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE", + "exemptedMembers": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json new file mode 100644 index 00000000000..11c2c7a3300 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json @@ -0,0 +1,110 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:28:39 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=147", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2322", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Up2HJY=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + }, + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json new file mode 100644 index 00000000000..fc4fff49b7d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-audit-wildcard-remove/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json @@ -0,0 +1,110 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 23:28:38 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=396", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2322", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Up2HJY=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "deleted:serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com?uid=117982589010933747015" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "storage.googleapis.com", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + }, + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_WRITE" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json new file mode 100644 index 00000000000..e0471820f02 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_1.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:38:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=239", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "27424", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "82151825516", + "projectId": "stkl-kody-mullins", + "lifecycleState": "ACTIVE", + "name": "stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-29T16:15:10.136Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_2.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_2.json new file mode 100644 index 00000000000..86ba6e04a85 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_2.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=192", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "27424", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "82151825516", + "projectId": "stkl-kody-mullins", + "lifecycleState": "ACTIVE", + "name": "stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-29T16:15:10.136Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_3.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_3.json new file mode 100644 index 00000000000..93c28b891c7 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/get-cloudresourcemanager.googleapis.com-v1-projects_3.json @@ -0,0 +1,36 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=189", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "27424", + "-content-encoding": "gzip", + "content-location": "https://cloudresourcemanager.googleapis.com/v1/projects?alt=json" + }, + "body": { + "projects": [ + { + "projectNumber": "82151825516", + "projectId": "stkl-kody-mullins", + "lifecycleState": "ACTIVE", + "name": "stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "createTime": "2026-09-29T16:15:10.136Z", + "parent": { + "type": "organization", + "id": "728386023487" + } + } + ] + } +} diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json new file mode 100644 index 00000000000..e1891380db9 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_1.json @@ -0,0 +1,86 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:38:59 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=123", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1822", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1JiT0cg=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json new file mode 100644 index 00000000000..26d564d2c27 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_2.json @@ -0,0 +1,99 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=245", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2034", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jjn4PI=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + }, + { + "logType": "DATA_WRITE" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json new file mode 100644 index 00000000000..7a6bafbb4e3 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_3.json @@ -0,0 +1,99 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=125", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2034", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jjn4PI=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + }, + { + "logType": "DATA_WRITE" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_4.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_4.json new file mode 100644 index 00000000000..0036f93d737 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_4.json @@ -0,0 +1,96 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=123", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1979", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jj4x6o=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_5.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_5.json new file mode 100644 index 00000000000..94128badf0a --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_5.json @@ -0,0 +1,96 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=143", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1979", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jj4x6o=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_6.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_6.json new file mode 100644 index 00000000000..1b42db32406 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-getIamPolicy_6.json @@ -0,0 +1,105 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:02 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=127", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2283", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1JkJiVQ=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json new file mode 100644 index 00000000000..8741407cb94 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_1.json @@ -0,0 +1,99 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:00 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=403", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2034", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jjn4PI=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + }, + { + "logType": "DATA_WRITE" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_2.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_2.json new file mode 100644 index 00000000000..597e1579407 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_2.json @@ -0,0 +1,96 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:01 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=401", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1979", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1Jj4x6o=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_3.json b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_3.json new file mode 100644 index 00000000000..71f95c9fa63 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/project-set-iam-policy-audit-configs/post-cloudresourcemanager.googleapis.com-v1-projects-stkl-kody-mullins-setIamPolicy_3.json @@ -0,0 +1,105 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Wed, 14 Oct 2026 22:39:02 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "server-timing": "gfet4t7; dur=509", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "2283", + "-content-encoding": "gzip" + }, + "body": { + "version": 1, + "etag": "BwZd1JkJiVQ=", + "bindings": [ + { + "role": "roles/aiplatform.customCodeServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform-cc.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-aiplatform.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/aiplatform.tuningServiceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-vertex-tune.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.instanceGroupManagerServiceAgent", + "members": [ + "serviceAccount:82151825516@cloudservices.gserviceaccount.com" + ] + }, + { + "role": "roles/compute.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@compute-system.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/editor", + "members": [ + "serviceAccount:82151825516-compute@developer.gserviceaccount.com", + "user:user1@example.com", + "user:user2@example.com" + ] + }, + { + "role": "roles/logging.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-logging.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/logging.viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/owner", + "members": [ + "user:user2@example.com", + "user:user3@example.com" + ] + }, + { + "role": "roles/pubsub.serviceAgent", + "members": [ + "serviceAccount:service-82151825516@gcp-sa-pubsub.iam.gserviceaccount.com" + ] + }, + { + "role": "roles/viewer", + "members": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ], + "auditConfigs": [ + { + "service": "allServices", + "auditLogConfigs": [ + { + "logType": "DATA_READ", + "exemptedMembers": [ + "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/main.tf b/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/main.tf new file mode 100644 index 00000000000..64d4eb2d2cb --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/main.tf @@ -0,0 +1,36 @@ +provider "google" {} + +data "google_project" "project" {} + +resource "google_service_account" "sa" { + account_id = "c7n-audit-wildcard" + display_name = "C7N Audit Wildcard Test SA" +} + +# An untouched binding the audit-only setIamPolicy must preserve. +resource "google_project_iam_member" "sa_viewer" { + project = data.google_project.project.project_id + role = "roles/viewer" + member = "serviceAccount:${google_service_account.sa.email}" +} + +# Exemptions on allServices and on a specific service, so service: '*' has to reach both. +resource "google_project_iam_audit_config" "all_services" { + project = data.google_project.project.project_id + service = "allServices" + + audit_log_config { + log_type = "DATA_WRITE" + exempted_members = ["serviceAccount:${google_service_account.sa.email}"] + } +} + +resource "google_project_iam_audit_config" "storage" { + project = data.google_project.project.project_id + service = "storage.googleapis.com" + + audit_log_config { + log_type = "DATA_READ" + exempted_members = ["serviceAccount:${google_service_account.sa.email}"] + } +} diff --git a/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/tf_resources.json b/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/tf_resources.json new file mode 100644 index 00000000000..aaec78ea531 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_project_iam_audit_wildcard/tf_resources.json @@ -0,0 +1,86 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_project": { + "project": { + "auto_create_network": null, + "billing_account": "000000-000000-000000", + "deletion_policy": "PREVENT", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "folder_id": "", + "id": "projects/stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "name": "stkl-kody-mullins", + "number": "82151825516", + "org_id": "728386023487", + "project_id": "stkl-kody-mullins", + "tags": null, + "terraform_labels": { + "goog-terraform-provisioned": "true" + } + } + }, + "google_project_iam_audit_config": { + "all_services": { + "audit_log_config": [ + { + "exempted_members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ], + "log_type": "DATA_WRITE" + } + ], + "etag": "BwZd1UzGW+Y=", + "id": "stkl-kody-mullins/audit_config/allServices", + "project": "stkl-kody-mullins", + "service": "allServices" + }, + "storage": { + "audit_log_config": [ + { + "exempted_members": [ + "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com" + ], + "log_type": "DATA_READ" + } + ], + "etag": "BwZd1UzGW+Y=", + "id": "stkl-kody-mullins/audit_config/storage.googleapis.com", + "project": "stkl-kody-mullins", + "service": "storage.googleapis.com" + } + }, + "google_project_iam_member": { + "sa_viewer": { + "condition": [], + "etag": "BwZd1UzGW+Y=", + "id": "stkl-kody-mullins/roles/viewer/serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "project": "stkl-kody-mullins", + "role": "roles/viewer" + } + }, + "google_service_account": { + "sa": { + "account_id": "c7n-audit-wildcard", + "create_ignore_already_exists": null, + "deletion_policy": "DELETE", + "description": "", + "disabled": false, + "display_name": "C7N Audit Wildcard Test SA", + "email": "c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "id": "projects/cloud-custodian/serviceAccounts/c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "name": "projects/cloud-custodian/serviceAccounts/c7n-audit-wildcard@stkl-kody-mullins.iam.gserviceaccount.com", + "project": "stkl-kody-mullins", + "timeouts": null, + "unique_id": "108776348046598870543" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/main.tf b/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/main.tf new file mode 100644 index 00000000000..f1520d63a45 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/main.tf @@ -0,0 +1,25 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id +} + +data "google_project" "project" { + project_id = var.google_project_id +} + +resource "google_service_account" "sa" { + account_id = "c7n-audit-config-test" + display_name = "C7N Audit Config Test SA" + project = var.google_project_id +} + +# Give the SA a viewer role so bindings are present from the start; +# the combined test also exercises adding roles/logging.viewer via add-bindings. +resource "google_project_iam_member" "sa_viewer" { + project = var.google_project_id + role = "roles/viewer" + member = "serviceAccount:${google_service_account.sa.email}" +} diff --git a/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/tf_resources.json b/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/tf_resources.json new file mode 100644 index 00000000000..42fdddfd1d6 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_project_set_iam_policy_audit_config/tf_resources.json @@ -0,0 +1,56 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_project": { + "project": { + "auto_create_network": null, + "billing_account": "000000-000000-000000", + "deletion_policy": "PREVENT", + "effective_labels": { + "goog-terraform-provisioned": "true" + }, + "folder_id": "", + "id": "projects/stkl-kody-mullins", + "labels": { + "goog-terraform-provisioned": "true" + }, + "name": "stkl-kody-mullins", + "number": "82151825516", + "org_id": "728386023487", + "project_id": "stkl-kody-mullins", + "tags": null, + "terraform_labels": { + "goog-terraform-provisioned": "true" + } + } + }, + "google_project_iam_member": { + "sa_viewer": { + "condition": [], + "etag": "BwZd1JiT0cg=", + "id": "stkl-kody-mullins/roles/viewer/serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "project": "stkl-kody-mullins", + "role": "roles/viewer" + } + }, + "google_service_account": { + "sa": { + "account_id": "c7n-audit-config-test", + "create_ignore_already_exists": null, + "deletion_policy": "DELETE", + "description": "", + "disabled": false, + "display_name": "C7N Audit Config Test SA", + "email": "c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "id": "projects/cloud-custodian/serviceAccounts/c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "member": "serviceAccount:c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "name": "projects/cloud-custodian/serviceAccounts/c7n-audit-config-test@stkl-kody-mullins.iam.gserviceaccount.com", + "project": "stkl-kody-mullins", + "timeouts": null, + "unique_id": "117982589010933747015" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_resourcemanager.py b/tools/c7n_gcp/tests/test_resourcemanager.py index 7b2383290a2..4298ef52a5f 100644 --- a/tools/c7n_gcp/tests/test_resourcemanager.py +++ b/tools/c7n_gcp/tests/test_resourcemanager.py @@ -1415,3 +1415,261 @@ def permissions(**data): assert permissions() == ('dlp.jobTriggers.list',) assert permissions(inherited=True) == ('dlp.jobTriggers.list', 'resourcemanager.projects.get') + + +@terraform("gcp_project_set_iam_policy_audit_config") +def test_project_set_iam_policy_audit_configs(test, gcp_project_set_iam_policy_audit_config): + project_id = gcp_project_set_iam_policy_audit_config.resources[ + 'google_project']['project']['project_id'] + sa_email = ( + 'serviceAccount:' + + gcp_project_set_iam_policy_audit_config.resources[ + 'google_service_account']['sa']['email'] + ) + + def audit_log_types(policy, service): + for ac in policy.get('auditConfigs', []): + if ac['service'] == service: + return {lc['logType'] for lc in ac.get('auditLogConfigs', [])} + return set() + + def exempted_members(policy, service, log_type): + for ac in policy.get('auditConfigs', []): + if ac['service'] == service: + for lc in ac.get('auditLogConfigs', []): + if lc['logType'] == log_type: + return set(lc.get('exemptedMembers', [])) + return set() + + def wait_for_policy(client, predicate, attempts=10): + # IAM policy writes are eventually consistent. Replay consumes the same + # recorded getIamPolicy responses in order, so the loop is deterministic. + for _ in range(attempts): + current = client.execute_query('getIamPolicy', {'resource': project_id}) + if predicate(current): + break + if test.recording: + time.sleep(3) + return current + + factory = test.replay_flight_data( + 'project-set-iam-policy-audit-configs', + project_id=project_id, + ) + policy1 = test.load_policy( + { + 'name': 'add-audit-configs', + 'resource': 'gcp.project', + 'filters': [{'type': 'value', 'key': 'projectId', 'value': project_id}], + 'actions': [ + { + 'type': 'set-iam-policy', + 'add-audit-configs': [ + { + 'service': 'allServices', + 'audit-log-configs': [ + {'log-type': 'DATA_READ'}, + {'log-type': 'DATA_WRITE'}, + ], + } + ], + } + ], + }, + session_factory=factory, + ) + resources = policy1.run() + assert len(resources) == 1 + + client1 = policy1.resource_manager.get_client() + updated1 = wait_for_policy( + client1, + lambda p: {'DATA_READ', 'DATA_WRITE'} <= audit_log_types(p, 'allServices'), + ) + log_types1 = audit_log_types(updated1, 'allServices') + assert 'DATA_READ' in log_types1 + assert 'DATA_WRITE' in log_types1 + + policy2 = test.load_policy( + { + 'name': 'remove-audit-config', + 'resource': 'gcp.project', + 'filters': [{'type': 'value', 'key': 'projectId', 'value': project_id}], + 'actions': [ + { + 'type': 'set-iam-policy', + 'remove-audit-configs': [ + { + 'service': 'allServices', + 'audit-log-configs': [{'log-type': 'DATA_WRITE'}], + } + ], + } + ], + }, + session_factory=factory, + ) + resources = policy2.run() + assert len(resources) == 1 + + client2 = policy2.resource_manager.get_client() + updated2 = wait_for_policy( + client2, + lambda p: 'DATA_WRITE' not in audit_log_types(p, 'allServices'), + ) + log_types2 = audit_log_types(updated2, 'allServices') + assert 'DATA_READ' in log_types2 + assert 'DATA_WRITE' not in log_types2 + + policy3 = test.load_policy( + { + 'name': 'audit-with-bindings', + 'resource': 'gcp.project', + 'filters': [{'type': 'value', 'key': 'projectId', 'value': project_id}], + 'actions': [ + { + 'type': 'set-iam-policy', + 'add-audit-configs': [ + { + 'service': 'allServices', + 'audit-log-configs': [ + { + 'log-type': 'DATA_READ', + 'exempted-members': [sa_email], + } + ], + } + ], + 'add-bindings': [ + { + 'role': 'roles/logging.viewer', + 'members': [sa_email], + } + ], + } + ], + }, + session_factory=factory, + ) + resources = policy3.run() + assert len(resources) == 1 + + client3 = policy3.resource_manager.get_client() + updated3 = wait_for_policy( + client3, + lambda p: sa_email in exempted_members(p, 'allServices', 'DATA_READ') and any( + b['role'] == 'roles/logging.viewer' and sa_email in b['members'] + for b in p.get('bindings', [])), + ) + + exempted = exempted_members(updated3, 'allServices', 'DATA_READ') + assert sa_email in exempted + + bindings3 = {b['role']: b['members'] for b in updated3.get('bindings', [])} + assert sa_email in bindings3.get('roles/logging.viewer', []) + assert sa_email in bindings3.get('roles/viewer', []) + + +def audit_log_configs(policy): + return { + (ac['service'], lc['logType']): set(lc.get('exemptedMembers', [])) + for ac in policy.get('auditConfigs', []) for lc in ac.get('auditLogConfigs', [])} + + +def wait_for_iam_policy(test, client, project_id, predicate, attempts=10): + # IAM policy writes are eventually consistent. Replay consumes the same + # recorded getIamPolicy responses in order, so the loop is deterministic. + for _ in range(attempts): + current = client.execute_query('getIamPolicy', {'resource': project_id}) + if predicate(current): + break + if test.recording: + time.sleep(3) + return current + + +def run_audit_wildcard_policy(test, fixture, flight, action): + project_id = fixture.resources['google_project']['project']['project_id'] + sa_email = 'serviceAccount:' + fixture.resources['google_service_account']['sa']['email'] + factory = test.replay_flight_data(flight, project_id=project_id) + p = test.load_policy({ + 'name': flight, + 'resource': 'gcp.project', + 'filters': [ + {'type': 'value', 'key': 'projectId', 'value': project_id}, + {'type': 'iam-policy', 'doc': { + 'key': 'auditConfigs[].auditLogConfigs[].exemptedMembers[]', + 'value': 'not-null'}}, + ], + 'actions': [dict({'type': 'set-iam-policy'}, **action)], + }, session_factory=factory) + + assert len(p.run()) == 1 + + updated = wait_for_iam_policy( + test, p.resource_manager.get_client(), project_id, + lambda policy: not any(audit_log_configs(policy).values())) + # The audit-only update must leave existing role bindings untouched. + bindings = {b['role']: b['members'] for b in updated.get('bindings', [])} + assert sa_email in bindings.get('roles/viewer', []) + return audit_log_configs(updated) + + +CLEAR_ALL_EXEMPTIONS = [{'service': '*', 'audit-log-configs': [ + {'log-type': t, 'exempted-members': '*'} + for t in ('ADMIN_READ', 'DATA_READ', 'DATA_WRITE')]}] + + +@terraform("gcp_project_iam_audit_wildcard") +def test_project_set_iam_policy_audit_wildcard_remove(test, gcp_project_iam_audit_wildcard): + configs = run_audit_wildcard_policy( + test, gcp_project_iam_audit_wildcard, 'project-audit-wildcard-remove', + {'remove-audit-configs': CLEAR_ALL_EXEMPTIONS}) + + # Exemptions are cleared on every service, but each log type stays enabled. + assert configs[('allServices', 'DATA_WRITE')] == set() + assert configs[('storage.googleapis.com', 'DATA_READ')] == set() + assert not any(configs.values()) + + +@terraform("gcp_project_iam_audit_wildcard") +def test_project_set_iam_policy_audit_wildcard_add_remove(test, gcp_project_iam_audit_wildcard): + configs = run_audit_wildcard_policy( + test, gcp_project_iam_audit_wildcard, 'project-audit-wildcard-add-remove', { + 'add-audit-configs': [{'service': 'allServices', 'audit-log-configs': [ + {'log-type': t} for t in ('ADMIN_READ', 'DATA_READ', 'DATA_WRITE')]}], + 'remove-audit-configs': CLEAR_ALL_EXEMPTIONS, + }) + + # One setIamPolicy enables every log type and clears every exemption. + for log_type in ('ADMIN_READ', 'DATA_READ', 'DATA_WRITE'): + assert configs[('allServices', log_type)] == set() + assert configs[('storage.googleapis.com', 'DATA_READ')] == set() + assert not any(configs.values()) + + +def set_iam_policy_action(test, **data): + p = test.load_policy({ + 'name': 'set-iam-policy-audit-configs', + 'resource': 'gcp.project', + 'actions': [dict({'type': 'set-iam-policy'}, **data)], + }, validate=True) + return p.resource_manager.actions[0] + + +@pytest.mark.parametrize('data, match', [ + ({'add-audit-configs': [ + {'service': '*', 'audit-log-configs': [{'log-type': 'DATA_READ'}]}]}, + "add-audit-configs does not support service"), + ({'remove-audit-configs': [ + {'service': '*', 'audit-log-configs': [ + {'log-type': 'DATA_READ', 'exempted-members': '*'}, {'log-type': 'DATA_WRITE'}]}]}, + "requires exempted-members"), + ({'add-audit-configs': [ + {'service': 'allServices', 'audit-log-configs': [ + {'log-type': 'DATA_READ', 'exempted-members': '*'}]}]}, + None), +]) +def test_set_iam_policy_audit_configs_wildcard_validation(test, data, match): + with pytest.raises(PolicyValidationError, match=match): + set_iam_policy_action(test, **data) From 95a39afec2c1e8d07a01c0317e2b61c9f6c655b6 Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Tue, 20 Oct 2026 05:46:38 +0300 Subject: [PATCH 61/65] gcp - subnet - set-flow-log - add logConfig parameters (#10642) --------- Co-authored-by: Daniel Lindsley Co-authored-by: Caleb Syring Co-authored-by: Kody Mullins Co-authored-by: Claude Opus 5.5 --- tools/c7n_gcp/c7n_gcp/resources/network.py | 84 +++++++++-- ...ud-custodian-aggregated-subnetworks_1.json | 50 +++++++ ...-subnetworks-apparent-beetle-subnet_1.json | 49 +++++++ ...-subnetworks-apparent-beetle-subnet_1.json | 32 +++++ ...ud-custodian-aggregated-subnetworks_1.json | 50 +++++++ ...l1-subnetworks-native-roughy-subnet_1.json | 42 ++++++ ...l1-subnetworks-native-roughy-subnet_1.json | 32 +++++ .../terraform/gcp_subnet_set_flow_log/main.tf | 26 ++++ .../gcp_subnet_set_flow_log/tf_resources.json | 76 ++++++++++ .../main.tf | 26 ++++ .../tf_resources.json | 76 ++++++++++ tools/c7n_gcp/tests/test_network.py | 133 +++++++++++++++++- 12 files changed, 663 insertions(+), 13 deletions(-) create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json create mode 100644 tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json create mode 100644 tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/tf_resources.json create mode 100644 tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/main.tf create mode 100644 tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/tf_resources.json diff --git a/tools/c7n_gcp/c7n_gcp/resources/network.py b/tools/c7n_gcp/c7n_gcp/resources/network.py index 861a8542f41..dcfb0fd21ad 100644 --- a/tools/c7n_gcp/c7n_gcp/resources/network.py +++ b/tools/c7n_gcp/c7n_gcp/resources/network.py @@ -98,37 +98,97 @@ def get_resource_params(self, model, resource): @Subnet.action_registry.register('set-flow-log') class SetFlowLog(SubnetAction): - """Enable vpc flow logs on a subnet. + """Enable or configure VPC flow logs on a subnet. - :example: Enable flow logs on all subnets + The ``state`` parameter controls whether flow logging is enabled or disabled. + Additional ``logConfig`` parameters map directly to the GCP subnetworks API: + https://cloud.google.com/compute/docs/reference/rest/v1/subnetworks/patch - .. yaml: + :example: Enable flow logs on all subnets that have them disabled - policies: - - name: flow-active - resource: gcp.subnet - filters: - - enableFlowLogs: empty - actions: - - set-flow-log + .. code-block:: yaml + + policies: + - name: flow-active + resource: gcp.subnet + filters: + - enableFlowLogs: empty + actions: + - set-flow-log + + :example: Enable flow logs with custom logConfig parameters + + .. code-block:: yaml + + policies: + - name: flow-log-configure + resource: gcp.subnet + filters: + - enableFlowLogs: empty + actions: + - type: set-flow-log + state: true + aggregationInterval: INTERVAL_5_SEC + flowSampling: 0.5 + metadata: INCLUDE_ALL_METADATA + filterExpr: "true" + + :example: Enable flow logs with custom metadata fields + + .. code-block:: yaml + + policies: + - name: flow-log-custom-metadata + resource: gcp.subnet + filters: + - enableFlowLogs: empty + actions: + - type: set-flow-log + state: true + metadata: CUSTOM_METADATA + metadataFields: + - src_instance + - dest_instance + - src_vpc """ schema = type_schema( 'set-flow-log', - state={'type': 'boolean', 'default': True}) + state={'type': 'boolean', 'default': True}, + aggregationInterval={ + 'type': 'string', + 'enum': [ + 'INTERVAL_5_SEC', 'INTERVAL_30_SEC', 'INTERVAL_1_MIN', + 'INTERVAL_5_MIN', 'INTERVAL_10_MIN', 'INTERVAL_15_MIN', + ], + }, + flowSampling={'type': 'number', 'minimum': 0.0, 'maximum': 1.0}, + metadata={ + 'type': 'string', + 'enum': ['EXCLUDE_ALL_METADATA', 'INCLUDE_ALL_METADATA', 'CUSTOM_METADATA'], + }, + metadataFields={'type': 'array', 'items': {'type': 'string'}}, + filterExpr={'type': 'string'}, + ) method_spec = {'op': 'patch'} method_perm = 'update' def get_resource_params(self, m, r): params = super(SetFlowLog, self).get_resource_params(m, r) + log_config = {'enable': self.data.get('state', True)} + for field in ('aggregationInterval', 'flowSampling', 'metadata', + 'metadataFields', 'filterExpr'): + if field in self.data: + log_config[field] = self.data[field] return { 'project': params['project'], 'region': params['region'], 'subnetwork': params['subnetwork'], 'body': { 'fingerprint': r['fingerprint'], - 'enableFlowLogs': self.data.get('state', True)} + 'logConfig': log_config, + } } diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json new file mode 100644 index 00000000000..36559d07db4 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json @@ -0,0 +1,50 @@ +{ + "headers": { + "etag": "_mDMyCB_joghqub3yk4h-yRuZGY=/qcru2oEddgA2X-qJ4Uwp_jpWTfE=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:22:40 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "45276", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/subnetworks?alt=json" + }, + "body": { + "kind": "compute#subnetworkAggregatedList", + "id": "projects/cloud-custodian/aggregated/subnetworks", + "items": { + "regions/us-central1": { + "subnetworks": [ + { + "kind": "compute#subnetwork", + "id": "8594914635421497354", + "creationTimestamp": "2026-03-15T12:22:13.589-07:00", + "name": "apparent-beetle-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/apparent-beetle-vpc", + "ipCidrRange": "10.2.0.0/16", + "gatewayAddress": "10.2.0.1", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet", + "privateIpGoogleAccess": false, + "fingerprint": "mrTGuvuTYXI=", + "allowSubnetCidrRoutesOverlap": false, + "enableFlowLogs": false, + "privateIpv6GoogleAccess": "DISABLE_GOOGLE_ACCESS", + "purpose": "PRIVATE", + "logConfig": { + "enable": false + }, + "stackType": "IPV4_ONLY" + } + ] + } + }, + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/subnetworks" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json new file mode 100644 index 00000000000..0a2fa25c2c0 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json @@ -0,0 +1,49 @@ +{ + "headers": { + "etag": "PMI-Crdv71g7cIeZHYd3tZxwjlA=/vP6Bz3HaDcVvWzriKJiqJG6MA68=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:22:43 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "1113", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet?alt=json" + }, + "body": { + "kind": "compute#subnetwork", + "id": "8594914635421497354", + "creationTimestamp": "2026-03-15T12:22:13.589-07:00", + "name": "apparent-beetle-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/apparent-beetle-vpc", + "ipCidrRange": "10.2.0.0/16", + "gatewayAddress": "10.2.0.1", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet", + "privateIpGoogleAccess": false, + "fingerprint": "_WooZcNuCJs=", + "allowSubnetCidrRoutesOverlap": false, + "enableFlowLogs": true, + "privateIpv6GoogleAccess": "DISABLE_GOOGLE_ACCESS", + "purpose": "PRIVATE", + "logConfig": { + "enable": true, + "aggregationInterval": "INTERVAL_5_SEC", + "flowSampling": 0.5, + "metadata": "CUSTOM_METADATA", + "metadataFields": [ + "src_instance", + "dest_instance", + "src_vpc", + "dest_vpc" + ], + "filterExpr": "true" + }, + "stackType": "IPV4_ONLY" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json new file mode 100644 index 00000000000..aaae613ee3d --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log-custom-metadata/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-apparent-beetle-subnet_1.json @@ -0,0 +1,32 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:22:41 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "858", + "-content-encoding": "gzip" + }, + "body": { + "kind": "compute#operation", + "id": "6102401416831875566", + "name": "operation-1773602561173-64d1506a1a92a-da0da7bd-96a7f4c0", + "operationType": "compute.subnetworks.patch", + "targetLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet", + "targetId": "8594914635421497354", + "status": "DONE", + "user": "markos@sixfeetup.com", + "progress": 100, + "insertTime": "2026-03-15T12:22:41.354-07:00", + "startTime": "2026-03-15T12:22:41.355-07:00", + "endTime": "2026-03-15T12:22:41.355-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/operations/operation-1773602561173-64d1506a1a92a-da0da7bd-96a7f4c0", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json new file mode 100644 index 00000000000..e4be8cbc69f --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-aggregated-subnetworks_1.json @@ -0,0 +1,50 @@ +{ + "headers": { + "etag": "ysULMGwQRoIV3X2x_cg4BSvzMcA=/PSCaoW2JlI88h08mSfvXYVFAvds=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:21:09 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "45270", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/subnetworks?alt=json" + }, + "body": { + "kind": "compute#subnetworkAggregatedList", + "id": "projects/cloud-custodian/aggregated/subnetworks", + "items": { + "regions/us-central1": { + "subnetworks": [ + { + "kind": "compute#subnetwork", + "id": "8299078460599089253", + "creationTimestamp": "2026-03-15T12:20:42.804-07:00", + "name": "native-roughy-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/native-roughy-vpc", + "ipCidrRange": "10.2.0.0/16", + "gatewayAddress": "10.2.0.1", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet", + "privateIpGoogleAccess": false, + "fingerprint": "jpPB3jgDLc8=", + "allowSubnetCidrRoutesOverlap": false, + "enableFlowLogs": false, + "privateIpv6GoogleAccess": "DISABLE_GOOGLE_ACCESS", + "purpose": "PRIVATE", + "logConfig": { + "enable": false + }, + "stackType": "IPV4_ONLY" + } + ] + } + }, + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/aggregated/subnetworks" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json new file mode 100644 index 00000000000..60b2b146911 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/get-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json @@ -0,0 +1,42 @@ +{ + "headers": { + "etag": "MR-xfOaoGx3z3kqlbLzQaCgq-kM=/secc1oLOPXc6AzMjTMdroP5BS1M=", + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:21:13 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "976", + "-content-encoding": "gzip", + "content-location": "https://compute.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet?alt=json" + }, + "body": { + "kind": "compute#subnetwork", + "id": "8299078460599089253", + "creationTimestamp": "2026-03-15T12:20:42.804-07:00", + "name": "native-roughy-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/native-roughy-vpc", + "ipCidrRange": "10.2.0.0/16", + "gatewayAddress": "10.2.0.1", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet", + "privateIpGoogleAccess": false, + "fingerprint": "KRU3h62VSwo=", + "allowSubnetCidrRoutesOverlap": false, + "enableFlowLogs": true, + "privateIpv6GoogleAccess": "DISABLE_GOOGLE_ACCESS", + "purpose": "PRIVATE", + "logConfig": { + "enable": true, + "aggregationInterval": "INTERVAL_5_SEC", + "flowSampling": 0.5, + "metadata": "INCLUDE_ALL_METADATA" + }, + "stackType": "IPV4_ONLY" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json new file mode 100644 index 00000000000..7744a375ee5 --- /dev/null +++ b/tools/c7n_gcp/tests/data/flights/subnet-set-flow-log/patch-compute-v1-projects-cloud-custodian-regions-us-central1-subnetworks-native-roughy-subnet_1.json @@ -0,0 +1,32 @@ +{ + "headers": { + "content-type": "application/json; charset=UTF-8", + "vary": "Origin, X-Origin, Referer", + "date": "Sun, 15 Mar 2026 19:21:11 GMT", + "server": "ESF", + "x-xss-protection": "0", + "x-frame-options": "SAMEORIGIN", + "x-content-type-options": "nosniff", + "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000", + "transfer-encoding": "chunked", + "status": "200", + "content-length": "856", + "-content-encoding": "gzip" + }, + "body": { + "kind": "compute#operation", + "id": "8618948267387566152", + "name": "operation-1773602470917-64d15014075bf-937b0994-56ed0481", + "operationType": "compute.subnetworks.patch", + "targetLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet", + "targetId": "8299078460599089253", + "status": "DONE", + "user": "markos@sixfeetup.com", + "progress": 100, + "insertTime": "2026-03-15T12:21:11.107-07:00", + "startTime": "2026-03-15T12:21:11.108-07:00", + "endTime": "2026-03-15T12:21:11.108-07:00", + "selfLink": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/operations/operation-1773602470917-64d15014075bf-937b0994-56ed0481", + "region": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1" + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/main.tf b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/main.tf new file mode 100644 index 00000000000..a98ce871deb --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/main.tf @@ -0,0 +1,26 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id + billing_project = var.google_project_id + user_project_override = true +} + +resource "random_pet" "server" { +} + +resource "google_compute_network" "vpc" { + name = "${random_pet.server.id}-vpc" + auto_create_subnetworks = "false" + routing_mode = "GLOBAL" +} + +resource "google_compute_subnetwork" "default" { + name = "${random_pet.server.id}-subnet" + ip_cidr_range = "10.2.0.0/16" + network = google_compute_network.vpc.name + region = "us-central1" + # flow logging is disabled by default — the test enables it via the action +} diff --git a/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/tf_resources.json b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/tf_resources.json new file mode 100644 index 00000000000..60fb0e69a90 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log/tf_resources.json @@ -0,0 +1,76 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_compute_network": { + "vpc": { + "auto_create_subnetworks": false, + "bgp_always_compare_med": false, + "bgp_best_path_selection_mode": "LEGACY", + "bgp_inter_region_cost": "", + "delete_bgp_always_compare_med": false, + "delete_default_routes_on_create": false, + "description": "", + "enable_ula_internal_ipv6": false, + "gateway_ipv4": "", + "id": "projects/cloud-custodian/global/networks/native-roughy-vpc", + "internal_ipv6_range": "", + "mtu": 0, + "name": "native-roughy-vpc", + "network_firewall_policy_enforcement_order": "AFTER_CLASSIC_FIREWALL", + "network_id": "3155296971164137627", + "network_profile": "", + "numeric_id": "3155296971164137627", + "params": [], + "project": "stacklet-test-policies", + "routing_mode": "GLOBAL", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/native-roughy-vpc", + "timeouts": null + } + }, + "google_compute_subnetwork": { + "default": { + "allow_subnet_cidr_routes_overlap": false, + "creation_timestamp": "2026-03-15T12:20:42.804-07:00", + "description": "", + "external_ipv6_prefix": "", + "fingerprint": null, + "gateway_address": "10.2.0.1", + "id": "projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet", + "internal_ipv6_prefix": "", + "ip_cidr_range": "10.2.0.0/16", + "ip_collection": null, + "ipv6_access_type": "", + "ipv6_cidr_range": "", + "ipv6_gce_endpoint": "", + "log_config": [], + "name": "native-roughy-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/native-roughy-vpc", + "params": [], + "private_ip_google_access": false, + "private_ipv6_google_access": "DISABLE_GOOGLE_ACCESS", + "project": "stacklet-test-policies", + "purpose": "PRIVATE", + "region": "us-central1", + "reserved_internal_range": "", + "role": "", + "secondary_ip_range": [], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/native-roughy-subnet", + "send_secondary_ip_range_if_empty": null, + "stack_type": "IPV4_ONLY", + "state": "", + "subnetwork_id": 8299078460599089253, + "timeouts": null + } + }, + "random_pet": { + "server": { + "id": "native-roughy", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/main.tf b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/main.tf new file mode 100644 index 00000000000..a98ce871deb --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/main.tf @@ -0,0 +1,26 @@ +variable "google_project_id" { + description = "GCP project ID" +} + +provider "google" { + project = var.google_project_id + billing_project = var.google_project_id + user_project_override = true +} + +resource "random_pet" "server" { +} + +resource "google_compute_network" "vpc" { + name = "${random_pet.server.id}-vpc" + auto_create_subnetworks = "false" + routing_mode = "GLOBAL" +} + +resource "google_compute_subnetwork" "default" { + name = "${random_pet.server.id}-subnet" + ip_cidr_range = "10.2.0.0/16" + network = google_compute_network.vpc.name + region = "us-central1" + # flow logging is disabled by default — the test enables it via the action +} diff --git a/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/tf_resources.json b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/tf_resources.json new file mode 100644 index 00000000000..fb9d5dd5cb3 --- /dev/null +++ b/tools/c7n_gcp/tests/terraform/gcp_subnet_set_flow_log_custom_metadata/tf_resources.json @@ -0,0 +1,76 @@ +{ + "pytest-terraform": 1, + "outputs": {}, + "resources": { + "google_compute_network": { + "vpc": { + "auto_create_subnetworks": false, + "bgp_always_compare_med": false, + "bgp_best_path_selection_mode": "LEGACY", + "bgp_inter_region_cost": "", + "delete_bgp_always_compare_med": false, + "delete_default_routes_on_create": false, + "description": "", + "enable_ula_internal_ipv6": false, + "gateway_ipv4": "", + "id": "projects/cloud-custodian/global/networks/apparent-beetle-vpc", + "internal_ipv6_range": "", + "mtu": 0, + "name": "apparent-beetle-vpc", + "network_firewall_policy_enforcement_order": "AFTER_CLASSIC_FIREWALL", + "network_id": "6641337280388315191", + "network_profile": "", + "numeric_id": "6641337280388315191", + "params": [], + "project": "stacklet-test-policies", + "routing_mode": "GLOBAL", + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/apparent-beetle-vpc", + "timeouts": null + } + }, + "google_compute_subnetwork": { + "default": { + "allow_subnet_cidr_routes_overlap": false, + "creation_timestamp": "2026-03-15T12:22:13.589-07:00", + "description": "", + "external_ipv6_prefix": "", + "fingerprint": null, + "gateway_address": "10.2.0.1", + "id": "projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet", + "internal_ipv6_prefix": "", + "ip_cidr_range": "10.2.0.0/16", + "ip_collection": null, + "ipv6_access_type": "", + "ipv6_cidr_range": "", + "ipv6_gce_endpoint": "", + "log_config": [], + "name": "apparent-beetle-subnet", + "network": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/global/networks/apparent-beetle-vpc", + "params": [], + "private_ip_google_access": false, + "private_ipv6_google_access": "DISABLE_GOOGLE_ACCESS", + "project": "stacklet-test-policies", + "purpose": "PRIVATE", + "region": "us-central1", + "reserved_internal_range": "", + "role": "", + "secondary_ip_range": [], + "self_link": "https://www.googleapis.com/compute/v1/projects/cloud-custodian/regions/us-central1/subnetworks/apparent-beetle-subnet", + "send_secondary_ip_range_if_empty": null, + "stack_type": "IPV4_ONLY", + "state": "", + "subnetwork_id": 8594914635421497354, + "timeouts": null + } + }, + "random_pet": { + "server": { + "id": "apparent-beetle", + "keepers": null, + "length": 2, + "prefix": null, + "separator": "-" + } + } + } +} \ No newline at end of file diff --git a/tools/c7n_gcp/tests/test_network.py b/tools/c7n_gcp/tests/test_network.py index 35df9680465..b369376a7b4 100644 --- a/tools/c7n_gcp/tests/test_network.py +++ b/tools/c7n_gcp/tests/test_network.py @@ -1,10 +1,12 @@ # Copyright The Cloud Custodian Authors. # SPDX-License-Identifier: Apache-2.0 import time +from unittest.mock import patch -from c7n_gcp.resources.network import Interconnect, InterconnectAttachment +from c7n_gcp.resources.network import Interconnect, InterconnectAttachment, SetFlowLog from gcp_common import BaseTest, event_data from googleapiclient.errors import HttpError + from pytest_terraform import terraform @@ -159,6 +161,135 @@ def test_subnet_set_private_api(self): self.assertEqual(result['privateIpGoogleAccess'], True) +@terraform('gcp_subnet_set_flow_log') +def test_subnet_set_flow_log(test, gcp_subnet_set_flow_log): + project_id = gcp_subnet_set_flow_log['google_compute_subnetwork.default.project'] + factory = test.replay_flight_data( + 'subnet-set-flow-log', project_id=project_id) + + subnet_name = gcp_subnet_set_flow_log['google_compute_subnetwork.default.name'] + region = gcp_subnet_set_flow_log['google_compute_subnetwork.default.region'] + policy = test.load_policy( + { + 'name': 'gcp-subnet-set-flow-log', + 'resource': 'gcp.subnet', + 'filters': [ + {'name': subnet_name}, + {'enableFlowLogs': 'empty'}, + ], + 'actions': [ + { + 'type': 'set-flow-log', + 'state': True, + 'aggregationInterval': 'INTERVAL_5_SEC', + 'flowSampling': 0.5, + 'metadata': 'INCLUDE_ALL_METADATA', + } + ], + }, + session_factory=factory, + ) + + with patch.object( + SetFlowLog, 'invoke_api', autospec=True, side_effect=SetFlowLog.invoke_api + ) as invoke_api: + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['name'] == subnet_name + + # Replay serves canned responses regardless of request body, so check + # the PATCH payload the action sent. + _, _, op_name, params = invoke_api.call_args.args + assert op_name == 'patch' + assert params['body']['logConfig'] == { + 'enable': True, + 'aggregationInterval': 'INTERVAL_5_SEC', + 'flowSampling': 0.5, + 'metadata': 'INCLUDE_ALL_METADATA', + } + + client = policy.resource_manager.get_client() + for _ in range(10): + subnet = client.execute_query( + 'get', {'project': project_id, 'region': region, 'subnetwork': subnet_name} + ) + if subnet.get('logConfig', {}).get('enable'): + break + if test.recording: + time.sleep(3) + else: + raise AssertionError('Timed out waiting for flow logs to be enabled') + + assert subnet['logConfig']['enable'] is True + assert subnet['logConfig']['aggregationInterval'] == 'INTERVAL_5_SEC' + assert subnet['logConfig']['flowSampling'] == 0.5 + assert subnet['logConfig']['metadata'] == 'INCLUDE_ALL_METADATA' + + +@terraform('gcp_subnet_set_flow_log_custom_metadata') +def test_subnet_set_flow_log_custom_metadata(test, gcp_subnet_set_flow_log_custom_metadata): + fixture = gcp_subnet_set_flow_log_custom_metadata + project_id = fixture['google_compute_subnetwork.default.project'] + factory = test.replay_flight_data( + 'subnet-set-flow-log-custom-metadata', project_id=project_id) + + subnet_name = fixture['google_compute_subnetwork.default.name'] + region = fixture['google_compute_subnetwork.default.region'] + policy = test.load_policy( + { + 'name': 'gcp-subnet-set-flow-log-custom-metadata', + 'resource': 'gcp.subnet', + 'filters': [ + {'name': subnet_name}, + ], + 'actions': [ + { + 'type': 'set-flow-log', + 'state': True, + 'metadata': 'CUSTOM_METADATA', + 'metadataFields': ['src_instance', 'dest_instance', 'src_vpc', 'dest_vpc'], + 'filterExpr': 'true', + } + ], + }, + session_factory=factory, + ) + + with patch.object( + SetFlowLog, 'invoke_api', autospec=True, side_effect=SetFlowLog.invoke_api + ) as invoke_api: + resources = policy.run() + assert len(resources) == 1 + assert resources[0]['name'] == subnet_name + + _, _, op_name, params = invoke_api.call_args.args + assert op_name == 'patch' + assert params['body']['logConfig'] == { + 'enable': True, + 'metadata': 'CUSTOM_METADATA', + 'metadataFields': ['src_instance', 'dest_instance', 'src_vpc', 'dest_vpc'], + 'filterExpr': 'true', + } + + client = policy.resource_manager.get_client() + for _ in range(10): + subnet = client.execute_query( + 'get', {'project': project_id, 'region': region, 'subnetwork': subnet_name} + ) + if subnet.get('logConfig', {}).get('enable'): + break + if test.recording: + time.sleep(3) + else: + raise AssertionError('Timed out waiting for flow logs to be enabled') + + assert subnet['logConfig']['enable'] is True + assert subnet['logConfig']['metadata'] == 'CUSTOM_METADATA' + assert set(subnet['logConfig']['metadataFields']) == { + 'src_instance', 'dest_instance', 'src_vpc', 'dest_vpc'} + assert subnet['logConfig']['filterExpr'] == 'true' + + class RouterTest(BaseTest): def test_router_query(self): project_id = self.project_id From 142aa2359e26144e6efc301e9238919a15a9efff Mon Sep 17 00:00:00 2001 From: Tim Penhey Date: Wed, 21 Oct 2026 09:59:46 +1300 Subject: [PATCH 62/65] k8s - add api call stats and metrics (#11197) * k8s - add api call stats and metrics Kubernetes policies fell back to the null api stats collector, so metadata.json carried no api-stats and no ApiCalls metric was emitted. Count each kubernetes client call by method name for the duration of a policy execution, recording the counts in the api-stats metadata in the same shape as aws, and emit ApiCalls and ApiTime metrics. Failed calls are counted too. * core - share api call stats between providers, add aws call timing Move api call counting, timing, metadata and metrics into a core base class, so a provider only needs to hook its client to record calls. Providers whose clients have no event hooks of their own record calls through a shared function. Move k8s and aws onto it. aws keeps its call names, metadata and interface, and now times each call between botocore's before-call and after-call events, emitting an ApiTime metric alongside ApiCalls. * core - lock api call stats updates, check aws stats cleanup Filters and actions make api calls from thread pools, so serialise updates to the call counts and total time. Check that an aws execution removes its event handlers using a client created from the cached session afterwards; a client keeps its own copy of the session's handlers, so one created during the execution can't show it. * core - round ApiTime metric, record aws transport errors Round the ApiTime metric to milliseconds, as full float precision is noise in metric output. Record aws calls that fail in transport, such as connection errors. botocore emits after-call-error rather than after-call for those, without the operation model, so before-call now keeps the call's name alongside its start time in the request context. --- c7n/output.py | 51 +++++++++++++++++ c7n/resources/aws.py | 66 +++++++++++++--------- docs/source/aws/usage.rst | 4 +- tests/test_aws.py | 78 +++++++++++++++++++++++++- tools/c7n_kube/c7n_kube/client.py | 32 ++++++++++- tools/c7n_kube/tests/test_api_stats.py | 64 +++++++++++++++++++++ 6 files changed, 266 insertions(+), 29 deletions(-) create mode 100644 tools/c7n_kube/tests/test_api_stats.py diff --git a/c7n/output.py b/c7n/output.py index 94c6b498f6f..cdb656d33fe 100644 --- a/c7n/output.py +++ b/c7n/output.py @@ -14,10 +14,12 @@ import os import shutil import tempfile +import threading import time import uuid from abc import ABC, abstractmethod +from collections import Counter from c7n.exceptions import InvalidOutputConfig from c7n.registry import PluginRegistry @@ -177,6 +179,55 @@ def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None): """ +# Api call stats collectors active for the current policy execution, for +# providers whose clients have no event hooks of their own to subscribe to. +_api_call_recorders = [] + + +def record_api_call(name, elapsed): + """Record an api call, and its duration in seconds, with the active collectors.""" + for r in _api_call_recorders: + r.record(name, elapsed) + + +class APICallStats(DeltaStats): + """Count api calls by name, and their total time, for a policy execution. + + Recorded counts are the api-stats execution metadata, and ApiCalls and + ApiTime metrics are emitted when the execution ends. Providers either + call `record_api_call` from their client, or subscribe the collector's + `record` to their client's own events. + """ + + def __init__(self, ctx, config=None): + super().__init__(ctx, config) + self.api_calls = Counter() + self.api_time = 0.0 + # Calls are recorded from the thread pools filters and actions use. + self.lock = threading.Lock() + + def get_snapshot(self): + return dict(self.api_calls) + + def get_metadata(self): + return self.get_snapshot() + + def record(self, name, elapsed): + with self.lock: + self.api_calls[name] += 1 + self.api_time += elapsed + + def __enter__(self): + _api_call_recorders.append(self) + self.push_snapshot() + + def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None): + _api_call_recorders.remove(self) + self.ctx.metrics.put_metric("ApiCalls", sum(self.api_calls.values()), "Count") + self.ctx.metrics.put_metric("ApiTime", round(self.api_time, 3), "Seconds") + self.pop_snapshot() + + @sys_stats_outputs.register('psutil', condition=HAVE_PSUTIL) class SystemStats(DeltaStats): """Collect process statistics via psutil as deltas over policy execution. diff --git a/c7n/resources/aws.py b/c7n/resources/aws.py index 33284a76e42..b3c3d25ff56 100644 --- a/c7n/resources/aws.py +++ b/c7n/resources/aws.py @@ -3,7 +3,7 @@ from c7n.provider import clouds, Provider -from collections import Counter, namedtuple +from collections import namedtuple import contextlib import copy import datetime @@ -45,7 +45,7 @@ # Output base implementations we extend. from c7n.output import ( Metrics, - DeltaStats, + APICallStats, BlobOutput, LogOutput, ) @@ -663,22 +663,12 @@ def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None): @api_stats_outputs.register('aws') -class ApiStats(DeltaStats): - - def __init__(self, ctx, config=None): - super(ApiStats, self).__init__(ctx, config) - self.api_calls = Counter() - - def get_snapshot(self): - return dict(self.api_calls) - - def get_metadata(self): - return self.get_snapshot() +class ApiStats(APICallStats): def __enter__(self): if isinstance(self.ctx.session_factory, credentials.SessionFactory): self.ctx.session_factory.set_subscribers((self,)) - self.push_snapshot() + super().__enter__() def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None): if isinstance(self.ctx.session_factory, credentials.SessionFactory): @@ -686,20 +676,46 @@ def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None): # With cached sessions, we need to unregister any events subscribers # on extant sessions to allow for the next registration. - utils.local_session(self.ctx.session_factory).events.unregister( - 'after-call.*.*', self._record, unique_id='c7n-api-stats') - - self.ctx.metrics.put_metric( - "ApiCalls", sum(self.api_calls.values()), "Count") - self.pop_snapshot() + events = utils.local_session(self.ctx.session_factory).events + for event, handler, unique_id in self._handlers(): + events.unregister(event, handler, unique_id=unique_id) + super().__exit__(exc_type, exc_value, exc_traceback) def __call__(self, s): - s.events.register( - 'after-call.*.*', self._record, unique_id='c7n-api-stats') + for event, handler, unique_id in self._handlers(): + s.events.register(event, handler, unique_id=unique_id) + + def _handlers(self): + return ( + ('before-call.*.*', self._start, 'c7n-api-stats-start'), + ('after-call.*.*', self._record, 'c7n-api-stats'), + ('after-call-error.*.*', self._record_error, 'c7n-api-stats-error'), + ) - def _record(self, http_response, parsed, model, **kwargs): - self.api_calls["%s.%s" % ( - model.service_model.endpoint_prefix, model.name)] += 1 + @staticmethod + def _call_name(model): + return "%s.%s" % (model.service_model.endpoint_prefix, model.name) + + def _start(self, model, context, **kwargs): + # The request context is shared by an operation's before-call event and + # its after-call or after-call-error event, so carries the call's name + # and start time between them. + context['c7n_api_stats'] = (self._call_name(model), time.perf_counter()) + + def _record(self, http_response, parsed, model, context=None, **kwargs): + name, start = (context or {}).get('c7n_api_stats', (self._call_name(model), None)) + self._finish(name, start) + + def _record_error(self, exception, context=None, **kwargs): + # Emitted when the request fails in transport, e.g. a connection + # error, with no operation model, so only calls seen by before-call + # can be recorded. + call = (context or {}).get('c7n_api_stats') + if call: + self._finish(*call) + + def _finish(self, name, start): + self.record(name, time.perf_counter() - start if start is not None else 0.0) @blob_outputs.register('s3') diff --git a/docs/source/aws/usage.rst b/docs/source/aws/usage.rst index 379e019c010..60cdd0f684d 100644 --- a/docs/source/aws/usage.rst +++ b/docs/source/aws/usage.rst @@ -58,8 +58,8 @@ specify a region:: When running the metrics in a centralized account or when centralizing to a specific region, additional account and region dimensions will be included. -Typically, the following 5 metrics are included: PolicyException, ResourceCount, ResourceTime, -ActionTime, and ApiCalls. Value zero will be recorded as well. To filter the metrics +Typically, the following 6 metrics are included: PolicyException, ResourceCount, ResourceTime, +ActionTime, ApiCalls, and ApiTime. Value zero will be recorded as well. To filter the metrics and save costs, you can use the ``active_metrics`` and/or ``ignore_zero`` query parameters:: custodian run -s . --metrics aws://?ignore_zero=true&active_metrics=ResourceCount,ApiCalls diff --git a/tests/test_aws.py b/tests/test_aws.py index 6ad23b8493a..60e913e61f4 100644 --- a/tests/test_aws.py +++ b/tests/test_aws.py @@ -13,7 +13,7 @@ from c7n.exceptions import PolicyValidationError, InvalidOutputConfig from c7n.resources import aws, load_resources from c7n.schema import StructureParser -from c7n import output, schema +from c7n import credentials, output, schema, utils # resolver test needs to patch out thread usage from c7n.resources.sqs import SQS @@ -24,8 +24,11 @@ from aws_xray_sdk.core.models.segment import Segment from aws_xray_sdk.core.models.subsegment import Subsegment +import boto3 import pytest import vcr +from botocore.exceptions import ClientError, EndpointConnectionError +from botocore.stub import Stubber class TraceDoc(Bag): @@ -357,6 +360,79 @@ def test_metrics_query_params(self): assert aws_api.call_count == 1 +class ApiStatsTest(BaseTest): + + def stubbed_sqs(self, session): + client = session.client( + 'sqs', region_name='us-east-1', + aws_access_key_id='AKID', aws_secret_access_key='SECRET') + stubber = Stubber(client) + stubber.activate() + self.addCleanup(stubber.deactivate) + return client, stubber + + def test_records_calls_by_operation(self): + stats = aws.ApiStats(Bag(), Config.empty()) + session = boto3.Session(region_name='us-east-1') + stats(session) + client, stubber = self.stubbed_sqs(session) + stubber.add_response('list_queues', {}) + stubber.add_response('list_queues', {}) + stubber.add_client_error('get_queue_url', 'QueueDoesNotExist') + + client.list_queues() + client.list_queues() + with self.assertRaises(ClientError): + client.get_queue_url(QueueName='missing') + + self.assertEqual( + stats.get_metadata(), {'sqs.ListQueues': 2, 'sqs.GetQueueUrl': 1}) + self.assertGreater(stats.api_time, 0) + + def test_records_transport_errors(self): + stats = aws.ApiStats(Bag(), Config.empty()) + session = boto3.Session(region_name='us-east-1') + stats(session) + client = session.client( + 'sqs', region_name='us-east-1', + aws_access_key_id='AKID', aws_secret_access_key='SECRET') + error = EndpointConnectionError(endpoint_url='https://sqs.us-east-1.amazonaws.com') + + with patch.object(client._endpoint, 'make_request', side_effect=error): + with self.assertRaises(EndpointConnectionError): + client.list_queues() + + self.assertEqual(stats.get_metadata(), {'sqs.ListQueues': 1}) + self.assertGreater(stats.api_time, 0) + + def test_execution_emits_metrics(self): + utils.reset_session_cache() + self.addCleanup(utils.reset_session_cache) + factory = credentials.SessionFactory('us-east-1') + metrics = Mock() + stats = aws.ApiStats(Bag(session_factory=factory, metrics=metrics), Config.empty()) + + with stats: + client, stubber = self.stubbed_sqs(utils.local_session(factory)) + stubber.add_response('list_queues', {}) + client.list_queues() + + # A client keeps a copy of its session's event handlers, so check + # cleanup with one created from the cached session after the execution. + client, stubber = self.stubbed_sqs(utils.local_session(factory)) + stubber.add_response('list_queues', {}) + client.list_queues() + + self.assertEqual(stats.get_metadata(), {'sqs.ListQueues': 1}) + self.assertEqual(factory._subscribers, ()) + self.assertEqual(output._api_call_recorders, []) + calls = {c.args[0]: c.args[1:] for c in metrics.put_metric.call_args_list} + self.assertEqual(calls['ApiCalls'], (1, 'Count')) + self.assertEqual(calls['ApiTime'][1], 'Seconds') + self.assertGreater(stats.api_time, 0) + self.assertEqual(calls['ApiTime'][0], round(stats.api_time, 3)) + + class OutputLogsTest(BaseTest): # cloud watch logging diff --git a/tools/c7n_kube/c7n_kube/client.py b/tools/c7n_kube/c7n_kube/client.py index 57a0f51ae8b..4d8fc724a31 100644 --- a/tools/c7n_kube/c7n_kube/client.py +++ b/tools/c7n_kube/c7n_kube/client.py @@ -16,13 +16,38 @@ import logging import os +import time from kubernetes import config, client from kubernetes.client import Configuration, ApiClient +from c7n.output import APICallStats, api_stats_outputs, record_api_call + log = logging.getLogger("custodian.k8s.client") +class InstrumentedAPI: + """Wraps a kubernetes api object (e.g. CoreV1Api) to record each public + method call, by method name, with its duration.""" + + def __init__(self, api): + self._api = api + + def __getattr__(self, name): + attr = getattr(self._api, name) + if name.startswith("_") or not callable(attr): + return attr + + def call(*args, **kwargs): + start = time.perf_counter() + try: + return attr(*args, **kwargs) + finally: + record_api_call(name, time.perf_counter() - start) + + return call + + class Session: def __init__(self, config_file=None): self.config_file = config_file @@ -35,4 +60,9 @@ def client(self, group, version): api_client = ApiClient(configuration=client_config) log.debug("connecting to %s" % (api_client.configuration.host)) # e.g. client.CoreV1Api() - return getattr(client, "%s%sApi" % (group, version))(api_client) + return InstrumentedAPI(getattr(client, "%s%sApi" % (group, version))(api_client)) + + +@api_stats_outputs.register("k8s") +class APIStats(APICallStats): + """Count kubernetes api calls by client method name, and their time.""" diff --git a/tools/c7n_kube/tests/test_api_stats.py b/tools/c7n_kube/tests/test_api_stats.py new file mode 100644 index 00000000000..05e1ec152b4 --- /dev/null +++ b/tools/c7n_kube/tests/test_api_stats.py @@ -0,0 +1,64 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import json +import os +from unittest.mock import MagicMock, patch + +from common_kube import KubeTest + +from c7n import output +from c7n.output import LogMetrics +from c7n_kube.client import APIStats, InstrumentedAPI + + +class TestAPIStats(KubeTest): + def test_policy_run_records_api_calls(self): + # The pagination flight replays 3 pages, so a policy run makes three + # list calls through the client. + factory = self.replay_flight_data("TestQueryPagination.test_pagination_multi_page.yaml") + output_dir = self.get_temp_dir() + p = self.load_policy( + {"name": "config-maps", "resource": "k8s.config-map"}, + session_factory=factory, + output_dir=output_dir, + ) + metrics = [] + with patch.object(LogMetrics, "_put_metrics", lambda self, ns, m: metrics.extend(m)): + resources = p.run() + self.assertEqual(len(resources), 6) + + with open(os.path.join(output_dir, "config-maps", "metadata.json")) as fh: + metadata = json.load(fh) + self.assertEqual(metadata["api-stats"], {"list_config_map_for_all_namespaces": 3}) + + by_name = {m["MetricName"]: m for m in metrics} + self.assertEqual(by_name["ApiCalls"]["Value"], 3) + self.assertEqual(by_name["ApiCalls"]["Unit"], "Count") + self.assertEqual(by_name["ApiTime"]["Unit"], "Seconds") + self.assertGreater(p.ctx.api_stats.api_time, 0) + self.assertEqual(by_name["ApiTime"]["Value"], round(p.ctx.api_stats.api_time, 3)) + self.assertEqual(output._api_call_recorders, []) + + def test_records_failed_calls(self): + api = MagicMock() + api.patch_namespaced_deployment.side_effect = ValueError("boom") + stats = APIStats(MagicMock()) + stats.__enter__() + try: + with self.assertRaises(ValueError): + InstrumentedAPI(api).patch_namespaced_deployment(name="x") + finally: + stats.__exit__() + self.assertEqual(stats.get_metadata(), {"patch_namespaced_deployment": 1}) + + def test_no_recording_outside_execution(self): + api = MagicMock() + api.list_namespace.return_value = "result" + stats = APIStats(MagicMock()) + self.assertEqual(InstrumentedAPI(api).list_namespace(), "result") + self.assertEqual(stats.get_metadata(), {}) + + def test_non_callable_attributes_pass_through(self): + api = MagicMock() + api.api_client = "the-api-client" + self.assertEqual(InstrumentedAPI(api).api_client, "the-api-client") From addc02c12f3bd03560ec20a2583ffce84fd808b7 Mon Sep 17 00:00:00 2001 From: Tim Penhey Date: Wed, 21 Oct 2026 11:14:44 +1300 Subject: [PATCH 63/65] gcp - add api call stats and metrics (#11242) * gcp - add api call stats and metrics gcp policies fell back to the null api stats collector, so metadata.json carried no api-stats and no ApiCalls metric was emitted. Record each gcp api call by its method id, such as iam.roles.list, with its duration, on the shared api call stats base. A call is recorded once however many attempts its retries take, and failed calls are counted too. * gcp - rename api stats test module to a unique name pytest can't import two test modules with the same basename in one run, and c7n_kube already has a test_api_stats.py. --- tools/c7n_gcp/c7n_gcp/client.py | 26 ++++++++++- tools/c7n_gcp/tests/test_gcp_api_stats.py | 56 +++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 tools/c7n_gcp/tests/test_gcp_api_stats.py diff --git a/tools/c7n_gcp/c7n_gcp/client.py b/tools/c7n_gcp/c7n_gcp/client.py index e188f5048ae..7f6ef0b38af 100644 --- a/tools/c7n_gcp/c7n_gcp/client.py +++ b/tools/c7n_gcp/c7n_gcp/client.py @@ -29,6 +29,7 @@ import os import socket import ssl +import time from contextlib import nullcontext as no_rate_limiter from urllib.error import URLError @@ -44,6 +45,8 @@ from retrying import retry +from c7n.output import APICallStats, api_stats_outputs, record_api_call + HTTPLIB_CA_BUNDLE = os.environ.get('HTTPLIB_CA_BUNDLE') GOOGLE_IMPERSONATE_SERVICE_ACCOUNT = os.environ.get('GOOGLE_IMPERSONATE_SERVICE_ACCOUNT') @@ -552,11 +555,27 @@ def execute_query(self, verb, verb_arguments): request = self._build_request(verb, verb_arguments) return self._execute(request) + def _execute(self, request): + """Run execute with retries and rate limiting, recording the api call. + + Args: + request (object): The HttpRequest object to execute. + + Returns: + dict: The response from the API. + """ + start = time.perf_counter() + try: + return self._execute_with_retry(request) + finally: + # e.g. compute.instances.list, once per call however many attempts + record_api_call(request.methodId, time.perf_counter() - start) + @retry(retry_on_exception=is_retryable_exception, wait_exponential_multiplier=1000, wait_exponential_max=10000, stop_max_attempt_number=5) - def _execute(self, request): + def _execute_with_retry(self, request): """Run execute with retries and rate limiting. Args: @@ -567,3 +586,8 @@ def _execute(self, request): """ with self._rate_limiter: return request.execute(http=self.http, num_retries=self._num_retries) + + +@api_stats_outputs.register('gcp') +class ApiStats(APICallStats): + """Count gcp api calls by method id, e.g. compute.instances.list, and their time.""" diff --git a/tools/c7n_gcp/tests/test_gcp_api_stats.py b/tools/c7n_gcp/tests/test_gcp_api_stats.py new file mode 100644 index 00000000000..532fe57c1f0 --- /dev/null +++ b/tools/c7n_gcp/tests/test_gcp_api_stats.py @@ -0,0 +1,56 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import json +import os +from unittest.mock import MagicMock, patch + +from gcp_common import BaseTest + +from c7n import output +from c7n.output import LogMetrics +from c7n_gcp.client import ApiStats, ServiceClient + + +class ApiStatsTest(BaseTest): + + def test_policy_run_records_api_calls(self): + # The flight replays two pages of roles, so a policy run makes two + # list calls. + factory = self.replay_flight_data("ami-role-query", self.project_id) + output_dir = self.get_temp_dir() + p = self.load_policy( + {"name": "iam-roles", "resource": "gcp.iam-role"}, + session_factory=factory, + output_dir=output_dir, + ) + metrics = [] + with patch.object(LogMetrics, "_put_metrics", lambda self, ns, m: metrics.extend(m)): + resources = p.run() + self.assertEqual(len(resources), 2) + + with open(os.path.join(output_dir, "iam-roles", "metadata.json")) as fh: + metadata = json.load(fh) + self.assertEqual(metadata["api-stats"], {"iam.roles.list": 2}) + + by_name = {m["MetricName"]: m for m in metrics} + self.assertEqual(by_name["ApiCalls"]["Value"], 2) + self.assertEqual(by_name["ApiCalls"]["Unit"], "Count") + self.assertEqual(by_name["ApiTime"]["Unit"], "Seconds") + self.assertGreater(p.ctx.api_stats.api_time, 0) + self.assertEqual(by_name["ApiTime"]["Value"], round(p.ctx.api_stats.api_time, 3)) + self.assertEqual(output._api_call_recorders, []) + + def test_records_failed_calls(self): + client = ServiceClient.__new__(ServiceClient) + request = MagicMock(methodId="compute.instances.delete") + stats = ApiStats(MagicMock()) + stats.__enter__() + try: + with patch.object( + ServiceClient, "_execute_with_retry", side_effect=ValueError("boom") + ): + with self.assertRaises(ValueError): + client._execute(request) + finally: + stats.__exit__() + self.assertEqual(stats.get_metadata(), {"compute.instances.delete": 1}) From 0fa7d19f52284cd5af9ade2dc2b1d01eec9bdaf7 Mon Sep 17 00:00:00 2001 From: Tim Penhey Date: Thu, 22 Oct 2026 09:16:23 +1300 Subject: [PATCH 64/65] azure - add api call stats and metrics (#11243) azure policies fell back to the null api stats collector, so metadata.json carried no api-stats and no ApiCalls metric was emitted. Record each azure sdk request attempt, on the shared api call stats base, named by http method and resource type with resource names left out, such as GET Microsoft.Compute/virtualMachines; requests outside resource manager are named by host, leaving out the account or vault name. Clients get a custom hook policy that times each attempt, including ones that fail without a response, and still runs the existing hooks. The send override for legacy clients records each attempt in its retry loop. --- tools/c7n_azure/c7n_azure/session.py | 32 +++-- tools/c7n_azure/c7n_azure/utils.py | 72 +++++++++- .../ApiStatsTest.virtual_machine.json | 123 ++++++++++++++++++ .../tests_azure/test_azure_api_stats.py | 103 +++++++++++++++ .../c7n_azure/tests_azure/test_azure_utils.py | 11 +- 5 files changed, 327 insertions(+), 14 deletions(-) create mode 100644 tools/c7n_azure/tests_azure/cassettes/ApiStatsTest.virtual_machine.json create mode 100644 tools/c7n_azure/tests_azure/test_azure_api_stats.py diff --git a/tools/c7n_azure/c7n_azure/session.py b/tools/c7n_azure/c7n_azure/session.py index 0e4ddf0e548..0159e659b2e 100644 --- a/tools/c7n_azure/c7n_azure/session.py +++ b/tools/c7n_azure/c7n_azure/session.py @@ -19,8 +19,10 @@ from msrestazure.azure_cloud import AZURE_PUBLIC_CLOUD from requests import HTTPError +from c7n.output import APICallStats, api_stats_outputs + from c7n_azure import constants -from c7n_azure.utils import (C7nRetryPolicy, ManagedGroupHelper, +from c7n_azure.utils import (ApiStatsHookPolicy, C7nRetryPolicy, ManagedGroupHelper, ResourceIdParser, StringUtils, cost_query_override_api_version, custodian_azure_send_override, @@ -256,19 +258,24 @@ def client(self, client, vault_url=None): base_url=self.cloud_endpoints.endpoints.resource_manager) legacy = True else: + hooks = {'raw_response_hook': log_response_data} + # TODO: remove when azure-mgmt-costmanagement is upgraded past 1.x. + # https://github.com/Azure/azure-sdk-for-python/issues/17351 is closed, + # but 1.x still queries with api-version 2020-06-01, which is not + # supported, so replace it with 2019-11-01, which works as expected. + # Later releases default to a newer api-version, and honour the + # api_version keyword. + if client == 'azure.mgmt.costmanagement.CostManagementClient': + hooks['raw_request_hook'] = cost_query_override_api_version + client_args = { 'credential': self.credentials, - 'raw_response_hook': log_response_data, + # Runs the hooks, and records each request attempt for api stats. + 'custom_hook_policy': ApiStatsHookPolicy(**hooks), 'retry_policy': C7nRetryPolicy(), 'credential_scopes': [self.resource_endpoint + ".default"] } - # TODO: remove when fixed: https://github.com/Azure/azure-sdk-for-python/issues/17351 - # This workaround will replace used api-version for costmanagement requests - # 2020-06-01 is not supported, but 2019-11-01 is working as expected. - if client == 'azure.mgmt.costmanagement.CostManagementClient': - client_args['raw_request_hook'] = cost_query_override_api_version - # Handle Microsoft Graph API client configuration from c7n_azure.constants import MSGRAPH_RESOURCE_ID if self.resource_endpoint == MSGRAPH_RESOURCE_ID: @@ -400,3 +407,12 @@ def get_auth_endpoint(self, endpoint): return self.cloud_endpoints.endpoints.microsoft_graph_resource_id else: return getattr(self.cloud_endpoints.endpoints, endpoint) + + +@api_stats_outputs.register('azure') +class ApiStats(APICallStats): + """Count azure api requests by method and resource type, and their time. + + e.g. GET Microsoft.Compute/virtualMachines. Each retry of a request is + counted as a call. + """ diff --git a/tools/c7n_azure/c7n_azure/utils.py b/tools/c7n_azure/c7n_azure/utils.py index 7e30eeb7f25..2c7018efea6 100644 --- a/tools/c7n_azure/c7n_azure/utils.py +++ b/tools/c7n_azure/c7n_azure/utils.py @@ -11,14 +11,16 @@ import time import typing import uuid +from urllib.parse import urlsplit from concurrent.futures import as_completed from functools import lru_cache -from azure.core.pipeline.policies import RetryMode, RetryPolicy +from azure.core.pipeline.policies import CustomHookPolicy, RetryMode, RetryPolicy from azure.graphrbac.models import DirectoryObject, GetObjectsParameters from azure.identity import ManagedIdentityCredential from azure.keyvault.secrets import SecretClient, SecretProperties from azure.mgmt.web.models import NameValuePair +from c7n.output import record_api_call from c7n.utils import chunks, local_session from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id @@ -128,7 +130,11 @@ def custodian_azure_send_override(self, request, headers=None, content=None, **k retries = 0 max_retries = 8 while retries < max_retries: - response = self.orig_send(request, headers, content, **kwargs) + start = time.perf_counter() + try: + response = self.orig_send(request, headers, content, **kwargs) + finally: + record_api_call(api_call_name(request.method, request.url), time.perf_counter() - start) send_logger.debug(response.status_code) for k, v in response.headers.items(): @@ -697,6 +703,68 @@ def log_response_data(response): send_logger.debug(k + ':' + v) +# The first path segment of resource manager urls, as opposed to data plane +# urls such as storage accounts and key vaults. +ARM_PATH_ROOTS = ('subscriptions', 'providers', 'tenants') + + +def api_call_name(method, url): + """Name an api call by its http method and the resource type it is on. + + Resource manager calls are named by resource type, taken from the url path + with resource names left out, e.g. GET Microsoft.Compute/virtualMachines; + an action or child resource type on a resource follows its type, e.g. POST + Microsoft.Compute/virtualMachines/powerOff. Other calls are named by host, + leaving out an account or vault name in its first label, e.g. GET + blob.core.windows.net. + """ + parts = urlsplit(url) + segments = [s for s in parts.path.split('/') if s] + lowered = [s.lower() for s in segments] + if lowered and lowered[0] in ARM_PATH_ROOTS: + if 'providers' in lowered: + # The last provider namespace, so an extension resource such as + # diagnostic settings on a vm is named by its own type. + i = len(lowered) - 1 - lowered[::-1].index('providers') + after = segments[i + 1:] + target = after[:1] + after[1::2] + else: + # e.g. subscriptions/{id}/resourceGroups/{name} + target = segments[0::2] + return "%s %s" % (method.upper(), "/".join(target)) + host = parts.hostname or '' + if host.count('.') > 2: + host = host.split('.', 1)[1] + return "%s %s" % (method.upper(), host) + + +class ApiStatsHookPolicy(CustomHookPolicy): + """Record each api request attempt, by name, with its duration. + + Used as a client's custom hook policy, which its pipeline runs inside the + retry policy, so each retry of a request is recorded as a call. + """ + + def on_request(self, request): + request.context['c7n_api_stats_start'] = time.perf_counter() + super().on_request(request) + + def on_response(self, request, response): + super().on_response(request, response) + self._record(request) + + def on_exception(self, request): + # A failed attempt with no response, e.g. a connection error. + self._record(request) + + def _record(self, request): + start = request.context.get('c7n_api_stats_start') + http_request = request.http_request + record_api_call( + api_call_name(http_request.method, http_request.url), + time.perf_counter() - start if start is not None else 0.0) + + # This workaround will replace used api-version for costmanagement requests # 2020-06-01 is not supported, but 2019-11-01 is working as expected. def cost_query_override_api_version(request): diff --git a/tools/c7n_azure/tests_azure/cassettes/ApiStatsTest.virtual_machine.json b/tools/c7n_azure/tests_azure/cassettes/ApiStatsTest.virtual_machine.json new file mode 100644 index 00000000000..c6e03cccabf --- /dev/null +++ b/tools/c7n_azure/tests_azure/cassettes/ApiStatsTest.virtual_machine.json @@ -0,0 +1,123 @@ +{ + "version": 1, + "interactions": [ + { + "request": { + "method": "GET", + "uri": "https://management.azure.com/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/providers/Microsoft.Compute/virtualMachines?api-version=2020-12-01", + "body": null, + "headers": {} + }, + "response": { + "status": { + "code": 200, + "message": "OK" + }, + "headers": { + "cache-control": [ + "no-cache" + ], + "content-type": [ + "application/json; charset=utf-8" + ], + "content-length": [ + "7019" + ], + "date": [ + "Mon, 24 May 2021 14:41:52 GMT" + ], + "x-ms-original-request-ids": [ + "04820a56-2374-430d-8be4-3fd61d5c3351", + "648b5e18-6b70-45fc-a953-1d70e65ba801" + ] + }, + "body": { + "data": { + "value": [ + { + "name": "cctestvm", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM/providers/Microsoft.Compute/virtualMachines/cctestvm", + "type": "Microsoft.Compute/virtualMachines", + "location": "southcentralus", + "tags": { + "testtag": "testvalue", + "schedule": "on=(M-U,8);off=(M-U,18);tz=pt" + }, + "properties": { + "vmId": "f6a6b9b2-9b99-45b0-8a18-025998fc74d0", + "hardwareProfile": { + "vmSize": "Standard_A1_v2" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "16.04.0-LTS", + "version": "latest", + "exactVersion": "16.04.202104290" + }, + "osDisk": { + "osType": "Linux", + "name": "cctestvm_OsDisk_1_517889dc284d4327990247d6d933e390", + "createOption": "FromImage", + "caching": "ReadWrite", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm/providers/Microsoft.Compute/disks/cctestvm_OsDisk_1_517889dc284d4327990247d6d933e390" + }, + "diskSizeGB": 30 + }, + "dataDisks": [ + { + "lun": 0, + "name": "cctestvm_disk2_c891bbcef098417b9c9eaed9b8af5e86", + "createOption": "Empty", + "caching": "None", + "managedDisk": { + "storageAccountType": "Standard_LRS", + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm/providers/Microsoft.Compute/disks/cctestvm_disk2_c891bbcef098417b9c9eaed9b8af5e86" + }, + "diskSizeGB": 1023, + "toBeDetached": false + } + ] + }, + "osProfile": { + "computerName": "vmmilaytvhe7nwq", + "adminUsername": "testuser", + "linuxConfiguration": { + "disablePasswordAuthentication": false, + "patchSettings": { + "patchMode": "ImageDefault" + } + }, + "secrets": [] + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/test_vm/providers/Microsoft.Network/networkInterfaces/myVMNic" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": true, + "storageUri": "https://milaytvhe7nwqsalinuxvm.blob.core.windows.net/" + } + }, + "provisioningState": "Succeeded" + }, + "resources": [ + { + "id": "/subscriptions/ea42f556-5106-4743-99b0-c129bfa71a47/resourceGroups/TEST_VM/providers/Microsoft.Compute/virtualMachines/cctestvm/extensions/script1" + } + ] + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/tools/c7n_azure/tests_azure/test_azure_api_stats.py b/tools/c7n_azure/tests_azure/test_azure_api_stats.py new file mode 100644 index 00000000000..da9f1b5267c --- /dev/null +++ b/tools/c7n_azure/tests_azure/test_azure_api_stats.py @@ -0,0 +1,103 @@ +# Copyright The Cloud Custodian Authors. +# SPDX-License-Identifier: Apache-2.0 +import json +import os +from unittest.mock import MagicMock, Mock, patch + +from azure.core.pipeline import PipelineContext, PipelineRequest, PipelineResponse +from azure.core.rest import HttpRequest + +from c7n import output +from c7n.output import LogMetrics +from c7n_azure.session import ApiStats +from c7n_azure.utils import ApiStatsHookPolicy, api_call_name, custodian_azure_send_override + +from .azure_common import BaseTest, cassette_name + +ARM = "https://management.azure.com" + + +class ApiStatsTest(BaseTest): + + @cassette_name('virtual_machine') + def test_policy_run_records_api_calls(self): + output_dir = self.get_temp_dir() + p = self.load_policy( + {'name': 'azure-vms', 'resource': 'azure.vm'}, output_dir=output_dir) + metrics = [] + with patch.object(LogMetrics, "_put_metrics", lambda self, ns, m: metrics.extend(m)): + p.run() + + with open(os.path.join(output_dir, 'azure-vms', 'metadata.json')) as fh: + metadata = json.load(fh) + self.assertEqual(metadata['api-stats'], {'GET Microsoft.Compute/virtualMachines': 1}) + + by_name = {m['MetricName']: m for m in metrics} + self.assertEqual(by_name['ApiCalls']['Value'], 1) + self.assertGreater(p.ctx.api_stats.api_time, 0) + self.assertEqual(by_name['ApiTime']['Value'], round(p.ctx.api_stats.api_time, 3)) + self.assertEqual(output._api_call_recorders, []) + + def test_api_call_name(self): + sub = ARM + "/subscriptions/0000/resourceGroups/rg1/providers" + cases = [ + (ARM + "/subscriptions/0000/providers/Microsoft.Compute/virtualMachines" + "?api-version=2020-12-01", 'get', 'GET Microsoft.Compute/virtualMachines'), + (sub + "/Microsoft.Compute/virtualMachines/vm1", 'GET', + 'GET Microsoft.Compute/virtualMachines'), + (sub + "/Microsoft.Compute/virtualMachines/vm1/powerOff", 'POST', + 'POST Microsoft.Compute/virtualMachines/powerOff'), + (sub + "/Microsoft.Sql/servers/s1/databases", 'GET', + 'GET Microsoft.Sql/servers/databases'), + (sub + "/Microsoft.Compute/virtualMachines/vm1/providers/" + "Microsoft.Insights/diagnosticSettings/ds1", 'PUT', + 'PUT Microsoft.Insights/diagnosticSettings'), + (ARM + "/subscriptions/0000/resourcegroups", 'GET', 'GET subscriptions/resourcegroups'), + (ARM + "/providers/Microsoft.Management/managementGroups/mg1/descendants", 'GET', + 'GET Microsoft.Management/managementGroups/descendants'), + ("https://acct.blob.core.windows.net/container/blob?comp=list", 'GET', + 'GET blob.core.windows.net'), + ("https://myvault.vault.azure.net/secrets/s1", 'GET', 'GET vault.azure.net'), + ("https://graph.microsoft.com/v1.0/users/u1", 'GET', 'GET graph.microsoft.com'), + ] + for url, method, expected in cases: + self.assertEqual(api_call_name(method, url), expected, url) + + def pipeline_request(self, method, url): + return PipelineRequest(HttpRequest(method, url), PipelineContext(None)) + + def test_hook_policy_records_attempts_and_runs_hooks(self): + request_hook, response_hook = Mock(), Mock() + policy = ApiStatsHookPolicy(raw_request_hook=request_hook, raw_response_hook=response_hook) + url = ARM + "/subscriptions/0000/providers/Microsoft.Web/sites" + stats = ApiStats(MagicMock()) + stats.__enter__() + try: + request = self.pipeline_request('GET', url) + policy.on_request(request) + response = PipelineResponse(request.http_request, Mock(), request.context) + policy.on_response(request, response) + # A retried attempt that fails without a response, e.g. a connection error. + policy.on_request(request) + policy.on_exception(request) + finally: + stats.__exit__() + + self.assertEqual(stats.get_metadata(), {'GET Microsoft.Web/sites': 2}) + self.assertEqual(request_hook.call_count, 2) + self.assertEqual(response_hook.call_count, 1) + + def test_legacy_send_records_each_attempt(self): + client = Mock() + throttled = Mock(status_code=429, headers={'Retry-After': '1'}) + ok = Mock(status_code=200, headers={}) + client.orig_send.side_effect = [throttled, ok] + request = Mock(method='GET', url="https://graph.windows.net/0000/users?api-version=1.6") + stats = ApiStats(MagicMock()) + stats.__enter__() + try: + with patch('c7n_azure.utils.time.sleep'): + self.assertIs(custodian_azure_send_override(client, request), ok) + finally: + stats.__exit__() + self.assertEqual(stats.get_metadata(), {'GET graph.windows.net': 2}) diff --git a/tools/c7n_azure/tests_azure/test_azure_utils.py b/tools/c7n_azure/tests_azure/test_azure_utils.py index 683d0edb107..c32c1712581 100644 --- a/tools/c7n_azure/tests_azure/test_azure_utils.py +++ b/tools/c7n_azure/tests_azure/test_azure_utils.py @@ -19,6 +19,9 @@ from .azure_common import DEFAULT_SUBSCRIPTION_ID, BaseTest +# A request as the legacy client send override receives it. +LEGACY_REQUEST = Mock(method='GET', url='https://graph.windows.net/tenant/users') + try: from importlib import reload except Exception: @@ -252,7 +255,7 @@ def test_custodian_azure_send_override_200(self, logger): 'status_code': 200 } mock.orig_send.return_value = type(str('response'), (), response_dict) - mock.send('') + mock.send(LEGACY_REQUEST) self.assertEqual(mock.orig_send.call_count, 1) self.assertEqual(logger.call_count, 2) @@ -268,7 +271,7 @@ def test_custodian_azure_send_override_429(self, logger_debug, logger_warning): 'status_code': 429 } mock.orig_send.return_value = type(str('response'), (), response_dict) - mock.send('') + mock.send(LEGACY_REQUEST) self.assertEqual(mock.orig_send.call_count, 8) self.assertEqual(logger_debug.call_count, 8) @@ -284,7 +287,7 @@ def test_custodian_azure_send_override_429_long_retry(self, logger): 'status_code': 429 } mock.orig_send.return_value = type(str('response'), (), response_dict) - mock.send('') + mock.send(LEGACY_REQUEST) self.assertEqual(mock.orig_send.call_count, 1) self.assertEqual(logger.call_count, 1) @@ -302,7 +305,7 @@ def test_custodian_azure_send_override_429_missingheader(self, logger_debug, log mock.orig_send.return_value = type(str('response'), (), response_dict) with patch('time.sleep', new_callable=time.sleep(0)): - mock.send('') + mock.send(LEGACY_REQUEST) self.assertEqual(mock.orig_send.call_count, 8) self.assertEqual(logger_debug.call_count, 8) From 15b4f8a26f6c148271fd529d8571d61c482a00e1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 22 Oct 2026 15:46:17 -0400 Subject: [PATCH 65/65] chore: bump prerelease versions to 0.9.54a1+0fa7d19f5 (ENG-9376) Upstream released 0.9.53.0, so the base moves to 0.9.54 and the alpha series restarts at a1; the workspace tools each tick one patch. Drop the tfparse exclude-newer-package override. It was added in #3 to admit tfparse 0.6.20 past the 7-day cooldown, but as a fixed date it now caps tfparse at releases before 2026-06-02. Without it the lock resolves tfparse 0.6.22, matching upstream. Co-Authored-By: Claude Opus 5.5 (1M context) --- c7n/version.py | 2 +- pyproject.toml | 3 +-- tools/c7n_awscc/pyproject.toml | 2 +- tools/c7n_azure/pyproject.toml | 2 +- tools/c7n_gcp/pyproject.toml | 2 +- tools/c7n_kube/pyproject.toml | 2 +- tools/c7n_left/pyproject.toml | 2 +- tools/c7n_mailer/pyproject.toml | 2 +- tools/c7n_oci/pyproject.toml | 2 +- tools/c7n_openstack/pyproject.toml | 2 +- tools/c7n_org/pyproject.toml | 2 +- tools/c7n_policystream/pyproject.toml | 2 +- tools/c7n_sphinxext/pyproject.toml | 2 +- tools/c7n_tencentcloud/pyproject.toml | 2 +- uv.lock | 28 +++++++++++++-------------- 15 files changed, 28 insertions(+), 29 deletions(-) diff --git a/c7n/version.py b/c7n/version.py index 9ad6fc746cc..1067aefab63 100644 --- a/c7n/version.py +++ b/c7n/version.py @@ -1,2 +1,2 @@ # Generated via tools/dev/devpkg.py -version = "0.9.53a1+bf0b50dbc" +version = "0.9.54a1+0fa7d19f5" diff --git a/pyproject.toml b/pyproject.toml index 2bdf82af4c7..27b4a49c3ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "urllib3>2", ] name = "c7n" -version = "0.9.53a1+bf0b50dbc" +version = "0.9.54a1+0fa7d19f5" description = "Cloud Custodian - Policy Rules Engine" readme = "README.md" classifiers = [ @@ -67,7 +67,6 @@ addons = [ [tool.uv] exclude-newer = "7 days" -exclude-newer-package = { tfparse = "2026-06-02T00:00:00Z" } [tool.uv.sources] c7n = { workspace = true } diff --git a/tools/c7n_awscc/pyproject.toml b/tools/c7n_awscc/pyproject.toml index a0d2e527a71..c1c3df5a13f 100644 --- a/tools/c7n_awscc/pyproject.toml +++ b/tools/c7n_awscc/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "jsonpatch<2.0,>=1.32", ] name = "c7n_awscc" -version = "0.1.38a1+bf0b50dbc" +version = "0.1.39a1+0fa7d19f5" readme = "readme.md" description = "Cloud Custodian - AWS Cloud Control Provider" classifiers = [ diff --git a/tools/c7n_azure/pyproject.toml b/tools/c7n_azure/pyproject.toml index cfd9444dc96..1a3f594fe12 100644 --- a/tools/c7n_azure/pyproject.toml +++ b/tools/c7n_azure/pyproject.toml @@ -89,7 +89,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_azure" -version = "0.7.52a1+bf0b50dbc" +version = "0.7.53a1+0fa7d19f5" description = "Cloud Custodian - Azure Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_gcp/pyproject.toml b/tools/c7n_gcp/pyproject.toml index e982722307b..f1c7fde3546 100644 --- a/tools/c7n_gcp/pyproject.toml +++ b/tools/c7n_gcp/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "retrying<2.0.0,>=1.3.3", ] name = "c7n_gcp" -version = "0.4.52a1+bf0b50dbc" +version = "0.4.53a1+0fa7d19f5" description = "Cloud Custodian - Google Cloud Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_kube/pyproject.toml b/tools/c7n_kube/pyproject.toml index 1487f5daf17..c71a409760a 100644 --- a/tools/c7n_kube/pyproject.toml +++ b/tools/c7n_kube/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "kubernetes>=10.0.1", ] name = "c7n_kube" -version = "0.2.52a1+bf0b50dbc" +version = "0.2.53a1+0fa7d19f5" description = "Cloud Custodian - Kubernetes Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_left/pyproject.toml b/tools/c7n_left/pyproject.toml index 58d2c7035ac..debea3b6676 100644 --- a/tools/c7n_left/pyproject.toml +++ b/tools/c7n_left/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tfparse<1.0,>=0.6", ] name = "c7n_left" -version = "0.3.39a1+bf0b50dbc" +version = "0.3.40a1+0fa7d19f5" description = "Custodian policies for IAAC definitions" readme = "README.md" classifiers = [ diff --git a/tools/c7n_mailer/pyproject.toml b/tools/c7n_mailer/pyproject.toml index fa9db3f1726..fd3e55eab0b 100644 --- a/tools/c7n_mailer/pyproject.toml +++ b/tools/c7n_mailer/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "types-six<2.0.0,>=1.16.10", ] name = "c7n_mailer" -version = "0.6.52a1+bf0b50dbc" +version = "0.6.53a1+0fa7d19f5" description = "Cloud Custodian - Reference Mailer" readme = "README.md" classifiers = [ diff --git a/tools/c7n_oci/pyproject.toml b/tools/c7n_oci/pyproject.toml index 789ab5465fe..cecc5006b42 100644 --- a/tools/c7n_oci/pyproject.toml +++ b/tools/c7n_oci/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "pyopenssl>=24" ] name = "c7n_oci" -version = "0.1.27a1+bf0b50dbc" +version = "0.1.28a1+0fa7d19f5" description = "Cloud Custodian - OCI Support" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_openstack/pyproject.toml b/tools/c7n_openstack/pyproject.toml index 0669c60b5f3..e4da3a84360 100644 --- a/tools/c7n_openstack/pyproject.toml +++ b/tools/c7n_openstack/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "openstacksdk==0.52.0", ] name = "c7n_openstack" -version = "0.1.43a1+bf0b50dbc" +version = "0.1.44a1+0fa7d19f5" description = "Cloud Custodian - OpenStack Provider" readme = "readme.md" classifiers = [ diff --git a/tools/c7n_org/pyproject.toml b/tools/c7n_org/pyproject.toml index 0c5a2d0993a..41bd31107a0 100644 --- a/tools/c7n_org/pyproject.toml +++ b/tools/c7n_org/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "click>=8.0", ] name = "c7n_org" -version = "0.6.52a1+bf0b50dbc" +version = "0.6.53a1+0fa7d19f5" description = "Cloud Custodian - Parallel Execution" readme = "README.md" classifiers = [ diff --git a/tools/c7n_policystream/pyproject.toml b/tools/c7n_policystream/pyproject.toml index 0d9bcf256cf..a90dac62d8d 100644 --- a/tools/c7n_policystream/pyproject.toml +++ b/tools/c7n_policystream/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "requests<3.0.0,>=2.22.0", ] name = "c7n_policystream" -version = "0.4.52a1+bf0b50dbc" +version = "0.4.53a1+0fa7d19f5" description = "Cloud Custodian - Git Commits as Logical Policy Changes" readme = "README.md" classifiers = [ diff --git a/tools/c7n_sphinxext/pyproject.toml b/tools/c7n_sphinxext/pyproject.toml index 14853269f2d..5cb6db148ed 100644 --- a/tools/c7n_sphinxext/pyproject.toml +++ b/tools/c7n_sphinxext/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "sphinx-rtd-theme>=1.0.0", ] name = "c7n_sphinxext" -version = "1.1.52a1+bf0b50dbc" +version = "1.1.53a1+0fa7d19f5" description = "Cloud Custodian - Sphinx Extensions" classifiers = [ "License :: OSI Approved :: Apache Software License", diff --git a/tools/c7n_tencentcloud/pyproject.toml b/tools/c7n_tencentcloud/pyproject.toml index 4d7b9d19f92..0c7eac8c762 100644 --- a/tools/c7n_tencentcloud/pyproject.toml +++ b/tools/c7n_tencentcloud/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "tencentcloud-sdk-python<4.0.0,>=3.0.783", ] name = "c7n_tencentcloud" -version = "0.1.35a1+bf0b50dbc" +version = "0.1.36a1+0fa7d19f5" description = "Cloud Custodian - Tencent Cloud Provider" readme = "readme.md" diff --git a/uv.lock b/uv.lock index 7c8f4a7a541..d267f7c6df6 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,7 @@ resolution-markers = [ [options] exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. -exclude-newer-span = "P5D" +exclude-newer-span = "P7D" [manifest] members = [ @@ -1513,7 +1513,7 @@ wheels = [ [[package]] name = "c7n" -version = "0.9.53" +version = "0.9.54a1+0fa7d19f5" source = { editable = "." } dependencies = [ { name = "argcomplete" }, @@ -1602,7 +1602,7 @@ lint = [ [[package]] name = "c7n-awscc" -version = "0.1.38" +version = "0.1.39a1+0fa7d19f5" source = { editable = "tools/c7n_awscc" } dependencies = [ { name = "boto3" }, @@ -1627,7 +1627,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-azure" -version = "0.7.52" +version = "0.7.53a1+0fa7d19f5" source = { editable = "tools/c7n_azure" } dependencies = [ { name = "adal" }, @@ -1814,7 +1814,7 @@ dev = [ [[package]] name = "c7n-gcp" -version = "0.4.52" +version = "0.4.53a1+0fa7d19f5" source = { editable = "tools/c7n_gcp" } dependencies = [ { name = "c7n" }, @@ -1855,7 +1855,7 @@ dev = [ [[package]] name = "c7n-kube" -version = "0.2.52" +version = "0.2.53a1+0fa7d19f5" source = { editable = "tools/c7n_kube" } dependencies = [ { name = "c7n" }, @@ -1884,7 +1884,7 @@ dev = [ [[package]] name = "c7n-left" -version = "0.3.39" +version = "0.3.40a1+0fa7d19f5" source = { editable = "tools/c7n_left" } dependencies = [ { name = "c7n" }, @@ -1917,7 +1917,7 @@ dev = [ [[package]] name = "c7n-mailer" -version = "0.6.52" +version = "0.6.53a1+0fa7d19f5" source = { editable = "tools/c7n_mailer" } dependencies = [ { name = "boto3" }, @@ -1983,7 +1983,7 @@ dev = [ [[package]] name = "c7n-oci" -version = "0.1.27" +version = "0.1.28a1+0fa7d19f5" source = { editable = "tools/c7n_oci" } dependencies = [ { name = "c7n" }, @@ -2008,7 +2008,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-openstack" -version = "0.1.43" +version = "0.1.44a1+0fa7d19f5" source = { editable = "tools/c7n_openstack" } dependencies = [ { name = "c7n" }, @@ -2035,7 +2035,7 @@ dev = [ [[package]] name = "c7n-org" -version = "0.6.52" +version = "0.6.53a1+0fa7d19f5" source = { editable = "tools/c7n_org" } dependencies = [ { name = "c7n" }, @@ -2058,7 +2058,7 @@ dev = [{ name = "pytest" }] [[package]] name = "c7n-policystream" -version = "0.4.52" +version = "0.4.53a1+0fa7d19f5" source = { editable = "tools/c7n_policystream" } dependencies = [ { name = "boto3" }, @@ -2094,7 +2094,7 @@ dev = [ [[package]] name = "c7n-sphinxext" -version = "1.1.52" +version = "1.1.53a1+0fa7d19f5" source = { editable = "tools/c7n_sphinxext" } dependencies = [ { name = "c7n" }, @@ -2121,7 +2121,7 @@ requires-dist = [ [[package]] name = "c7n-tencentcloud" -version = "0.1.35" +version = "0.1.36a1+0fa7d19f5" source = { editable = "tools/c7n_tencentcloud" } dependencies = [ { name = "c7n" },