Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
caad897
gcp - add Vertex AI evaluation run resource support (#11013)
jimfulton Aug 21, 2026
ace09fe
gcp - notebook-v2 - add get and update-metadata action (#10994)
jimfulton Aug 21, 2026
c5f66e5
aws - iam - retry concurrent tag updates (#11044)
1fanwang Aug 22, 2026
a694b6f
aws - retry InternalServerException by default (#11041)
mattheidelbaugh Aug 22, 2026
37231fc
aws - wafv2 - fix wafv2-enabled filter ignoring CLOUDFRONT scope (#11…
bdwyertech Aug 22, 2026
3f8ee95
aws - cleanrooms - add members filter to collaboration resource (#11032)
algobarb Aug 22, 2026
7e316d6
c7n-org - orgaccounts.py - use State rather than Status for active ac…
jordanst3wart Aug 22, 2026
df800e9
releng - dependency updates 2026-08 (#11045)
ajkerrigan Aug 22, 2026
8f458b3
azure - add machine-learning-model-version resource (#10872)
anxkhn Aug 25, 2026
3555adb
gcp - support regional persistent disks for disk actions (#10995)
jimfulton Aug 25, 2026
4d971e9
aws - sagemaker studio - add app, user-profile, space resources (#108…
jimfulton Aug 25, 2026
8f999d4
aws - add vpc-lattice-rule resource (#10565)
toastdriven Aug 26, 2026
4597a9b
azure - add budget resource (#10495)
calebsyring Aug 26, 2026
32c349d
gcp - cloud-run - drop non-schema fields from label mutations (#11052)
jtroup Aug 27, 2026
32ff971
azure - add private dns zone and record set resources (#11053)
jimfulton Aug 31, 2026
4cdfda0
aws - s3 output - configurable server-side encryption for policy logs…
strixthekiet Sep 1, 2026
de842db
aws - emr-serverless-app - add detail spec (#10928)
jimfulton Sep 1, 2026
2185ebd
azure - add machine learning job archive action (#11005)
jimfulton Sep 1, 2026
74e83ce
aws - cross-account filter - ForAnyValue Bugfix (#11074)
mattheidelbaugh Sep 1, 2026
4be0ee2
gcp - fix region-parented resource enumeration when no explicit regio…
jimfulton Sep 1, 2026
427a1a3
azure - functions - replace SCM basic auth with Entra ID bearer token…
nihalrm Sep 3, 2026
7ef6b61
k8s - fix resource listing to follow pagination continuation tokens (…
sontek Sep 3, 2026
e4c099e
tags - standardize dynamic tagging support (#10968)
ajkerrigan Sep 4, 2026
9c22e8b
aws - kms, route53 - exclude AWS-managed resources from actions (#11050)
UsmanQT Sep 4, 2026
f852abe
core - add a --vars-file parameter to inject variables (#10939)
adamsgoldsmith Sep 8, 2026
944447a
gcp - kms-cryptokey - add update action (#10910)
jimfulton Sep 8, 2026
8693ccc
releng - :seedling: bump the python-requirements group across 1 direc…
dependabot[bot] Sep 9, 2026
7ec4027
k8s - add custom-resource type for dynamic CRD discovery (#11083)
sontek Sep 9, 2026
2a85d60
aws - bedrock-custom-model - add deployments filter (#10944)
jimfulton Sep 10, 2026
951b906
gcp - dataproc-clusters - add audit mode support (#10913)
jimfulton Sep 10, 2026
6235eba
feat: aws - apigwv2 - add route resource for authorization type filte…
scott-sixfeetup Sep 14, 2026
6af640c
aws - cloudwatch-dashboard - use native cloudwatch apis for tagging (…
ajkerrigan Sep 15, 2026
bf0b50d
aws - tagging - tolerate resources deleted mid-run (#11089)
ajkerrigan Sep 15, 2026
a415910
chore: merge branch 'main' into elmo/firm-toad
jtroup Sep 15, 2026
28e92b0
chore: bump version suffix to a1+bf0b50dbc
jtroup Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions c7n/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import importlib
import logging
import os
import pathlib
import pdb
import sys
import traceback
Expand Down Expand Up @@ -74,10 +75,9 @@ def _default_options(p, exclude=[]):
output.add_argument("--debug", default=False, help=argparse.SUPPRESS,
action="store_true")

if 'vars' not in exclude:
# p.add_argument('--vars', default=None,
# help='Vars file to substitute into policy')
p.set_defaults(vars=None)
if 'vars-file' not in exclude:
p.add_argument('--vars-file', default=None, type=_valid_path,
help='Vars file to substitute into policy')

if 'log-group' not in exclude:
p.add_argument(
Expand Down Expand Up @@ -197,6 +197,17 @@ def _key_val_pair(value):
return value


def _valid_path(value):
"""
Type checker to ensure that --vars-file is a path to a file
"""
path = pathlib.Path(value)
if not path.is_file():
msg = 'vars-file must point to a file'
raise argparse.ArgumentTypeError(msg)
return value


def setup_parser():
c7n_desc = "Cloud Custodian - Cloud fleet management"
parser = argparse.ArgumentParser(description=c7n_desc)
Expand Down
15 changes: 9 additions & 6 deletions c7n/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright The Cloud Custodian Authors.
# SPDX-License-Identifier: Apache-2.0
from collections import Counter, defaultdict
from collections.abc import Mapping
from datetime import timedelta, datetime
from functools import wraps
import json
Expand Down Expand Up @@ -50,7 +51,7 @@ def _load_policies(options):
options.region = ""
for fp in options.configs:
try:
collection = policy_load(options, fp, validate=validate, vars=vars)
collection = policy_load(options, fp, validate=validate)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a vars file supplies a typed policy value such as max-resources: "{limit}", schema validation rejects the placeholder before p.expand_variables can restore the integer. Pass vars=vars to policy_load, or validate only after variable expansion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At c7n/commands.py, line 54:

<comment>When a vars file supplies a typed policy value such as `max-resources: "{limit}"`, schema validation rejects the placeholder before `p.expand_variables` can restore the integer. Pass `vars=vars` to `policy_load`, or validate only after variable expansion.</comment>

<file context>
@@ -50,7 +51,7 @@ def _load_policies(options):
         for fp in options.configs:
             try:
-                collection = policy_load(options, fp, validate=validate, vars=vars)
+                collection = policy_load(options, fp, validate=validate)
             except IOError:
                 log.error('policy file does not exist ({})'.format(fp))
</file context>
Suggested change
collection = policy_load(options, fp, validate=validate)
collection = policy_load(options, fp, validate=validate, vars=vars)

except IOError:
log.error('policy file does not exist ({})'.format(fp))
errors += 1
Expand Down Expand Up @@ -123,7 +124,7 @@ def _load_policies(options):

# Variable expansion and non schema validation (not optional)
for p in policies:
p.expand_variables(p.get_variables())
p.expand_variables(p.get_variables(variables=dict(vars or {})))
p.validate()

return f(options, list(policies))
Expand All @@ -133,11 +134,13 @@ def _load_policies(options):

def _load_vars(options):
vars = None
if options.vars:
if options.vars_file:
try:
vars = load_file(options.vars)
except IOError as e:
log.error('Problem loading vars file "{}": {}'.format(options.vars, e.strerror))
vars = load_file(options.vars_file)
if not isinstance(vars, Mapping):
raise ValueError("Invalid vars file type")
except (IOError, yaml.YAMLError, ValueError) as e:
log.error('Problem loading vars file "{}": {}'.format(options.vars_file, e))
sys.exit(1)

# TODO - provide builtin vars here (such as account)
Expand Down
1 change: 1 addition & 0 deletions c7n/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def empty(cls, **kw):
'region': os.environ.get('AWS_DEFAULT_REGION', 'us-east-1'),
'regions': (),
'cache': '',
'vars_file': None,
'profile': None,
'account_id': None,
'assume_role': None,
Expand Down
26 changes: 25 additions & 1 deletion c7n/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,34 @@
# Copyright The Cloud Custodian Authors.
# SPDX-License-Identifier: Apache-2.0

import typing

from c7n import deprecated
from c7n.executor import ThreadPoolExecutor
from c7n.utils import jmespath_search


class ElementJSONSchema(typing.TypedDict, total=False):
"""The shape of a filter/action's ``schema`` class attribute.

This is a JSON Schema (draft-07) fragment, not a standalone
document: it gets embedded into the combined policy schema (see
``c7n.schema.process_resource``) at
``#/definitions/resources/<resource>/(actions|filters)/<name>``,
and is what validates a single filter/action block in policy
YAML, e.g. ``{'type': 'update', 'rotationPeriod': '...'}``.

Elements normally build this via the ``type_schema()`` helper
(``c7n.utils.type_schema``) rather than writing it by hand.
"""

type: str
properties: dict[str, typing.Any]
required: list[str]
additionalProperties: bool
allOf: list[dict[str, typing.Any]]


class Element:
"""Parent base class for filters and actions.
"""
Expand All @@ -16,7 +39,8 @@ class Element:

executor_factory = ThreadPoolExecutor

schema = {'type': 'object'}
schema: ElementJSONSchema = {'type': 'object'}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the package is type-checked, this closed annotation makes existing element subclasses invalid: several schemas use top-level anyOf/oneOf, and AgeFilter.schema is intentionally None. Broaden ElementJSONSchema to cover the supported JSON Schema keywords and nullable schemas, or use a general JSON-schema mapping type for Element.schema.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At c7n/element.py, line 42:

<comment>When the package is type-checked, this closed annotation makes existing element subclasses invalid: several schemas use top-level `anyOf`/`oneOf`, and `AgeFilter.schema` is intentionally `None`. Broaden `ElementJSONSchema` to cover the supported JSON Schema keywords and nullable schemas, or use a general JSON-schema mapping type for `Element.schema`.</comment>

<file context>
@@ -16,7 +39,8 @@ class Element:
     executor_factory = ThreadPoolExecutor
 
-    schema = {'type': 'object'}
+    schema: ElementJSONSchema = {'type': 'object'}
+
     # schema aliases get hoisted into a jsonschema definition
</file context>


# schema aliases get hoisted into a jsonschema definition
# location, and then referenced inline.
schema_alias = None
Expand Down
43 changes: 43 additions & 0 deletions c7n/resources/apigw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,3 +1382,46 @@ def process(self, resources):
ApiId=r['c7n:parent-id'],
StageName=r['StageName']
)


@resources.register("apigwv2-route")
class ApiGatewayV2Route(query.ChildResourceManager):
"""Resource manager for API Gateway V2 Routes.

API Gateway V2 routes define the routing logic for HTTP and WebSocket APIs.
This resource enables filtering routes by authorization type to identify
routes that may lack proper authorization controls.

:example:

Find routes with no authorization configured:

.. code-block:: yaml

policies:
- name: apigwv2-routes-without-authorization
resource: aws.apigwv2-route
filters:
- type: value
key: AuthorizationType
value: NONE
"""

class resource_type(query.TypeInfo):
service = "apigatewayv2"
enum_spec = ('get_routes', 'Items', None)
parent_spec = ('aws.apigwv2', 'ApiId', True)
arn_type = "/apis"
id = "RouteId"
name = "RouteKey"
cfn_type = "AWS::ApiGatewayV2::Route"
permission_prefix = 'apigateway'
permissions_enum = ('apigateway:GET',)

def get_arns(self, resources):
partition = get_partition(self.config.region)
return [
"arn:{}:apigateway:{}::/apis/{}/routes/{}".format(
partition, self.config.region, r['c7n:parent-id'], r['RouteId']
)
for r in resources]
53 changes: 52 additions & 1 deletion c7n/resources/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
from c7n.manager import resources
from c7n.exceptions import PolicyValidationError
from c7n.query import QueryResourceManager, TypeInfo, DescribeSource, DescribeWithResourceTags
from c7n.query import RetryPageIterator
from c7n.tags import RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment
from c7n.utils import local_session, type_schema, QueryParser
from c7n.actions import BaseAction
from c7n.filters.kms import KmsRelatedFilter
from c7n.filters import MetricsFilter, ValueFilter
from c7n.filters import Filter, MetricsFilter, ValueFilter
from c7n.resources.aws import shape_schema, shape_validate, Arn
from c7n.resources.s3 import BucketAssembly, S3_AUGMENT_TABLE

Expand Down Expand Up @@ -225,6 +226,56 @@ class BedrockCustomModelKmsFilter(KmsRelatedFilter):
RelatedIdsExpression = 'modelKmsKeyArn'


@BedrockCustomModel.filter_registry.register('deployments')
class ModelDeploymentsFilter(Filter):
"""Filter custom models by their custom model deployments.

Queries ``ListCustomModelDeployments`` for deployments referencing the
model, optionally scoped server-side by ``status``. Matches
``value: present`` if any matching deployment is found, or
``value: absent`` if none is found.

:example:

Find custom models with no active deployment:

.. code-block:: yaml

policies:
- name: bedrock-custom-model-no-active-deployment
resource: aws.bedrock-custom-model
filters:
- type: deployments
status: Active
value: absent
"""
schema = type_schema(
'deployments',
status={'enum': ['Creating', 'Active', 'Failed']},
value={'enum': ['present', 'absent']},
required=['value'])
permissions = ('bedrock:ListCustomModelDeployments',)
annotation_key = 'c7n:Deployments'

def process(self, resources, event=None):
client = local_session(self.manager.session_factory).client('bedrock')
params = {}
if 'status' in self.data:
params['statusEquals'] = self.data['status']
want_present = self.data['value'] == 'present'
results = []
paginator = client.get_paginator('list_custom_model_deployments')
paginator.PAGE_ITERATOR_CLS = RetryPageIterator
for r in resources:
deployments = paginator.paginate(
modelArnEquals=r['modelArn'], **params
).build_full_result().get('modelDeploymentSummaries', [])
r[self.annotation_key] = deployments
if bool(deployments) == want_present:
results.append(r)
return results


class DescribeBedrockCustomizationJob(DescribeSource):

def augment(self, resources):
Expand Down
94 changes: 90 additions & 4 deletions c7n/resources/cw.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from concurrent.futures import as_completed
from datetime import datetime, timedelta

from c7n.actions import BaseAction
from botocore.exceptions import ClientError

from c7n.actions import AutoTagUser, BaseAction
from c7n.exceptions import PolicyValidationError
from c7n.filters import Filter, MetricsFilter
from c7n.filters.core import parse_date, ValueFilter
Expand All @@ -17,7 +19,8 @@
QueryResourceManager,
TypeInfo, DescribeSource, ConfigSource, DescribeWithResourceTags)
from c7n.resolver import ValuesFrom
from c7n.tags import universal_augment
from c7n.tags import (
RemoveTag, Tag, TagActionFilter, TagDelayedAction, universal_augment)
from c7n.utils import type_schema, local_session, chunks, get_retry, jmespath_search


Expand Down Expand Up @@ -688,6 +691,32 @@ def process(self, resources):
logGroupName=r['logGroupName'], **params)


class DescribeDashboard(DescribeSource):

def augment(self, resources):
# Dashboards aren't supported by the resource groups tagging api,
# fetch their tags from cloudwatch directly.
resources = super().augment(resources)
client = local_session(self.manager.session_factory).client('cloudwatch')
results = []
for r, arn in zip(resources, self.manager.get_arns(resources)):
try:
tags = self.manager.retry(
client.list_tags_for_resource, ResourceARN=arn).get('Tags', [])
except ClientError as e:
if e.response['Error']['Code'] not in (
'ResourceNotFound', 'ResourceNotFoundException'):
raise
# the dashboard was deleted between enumeration and augment
self.manager.log.warning(
"Resource not found: list_tags_for_resource using %s" % {
'ResourceARN': arn})
continue
r['Tags'] = tags
results.append(r)
return results


@resources.register("cloudwatch-dashboard")
class CloudWatchDashboard(QueryResourceManager):
class resource_type(TypeInfo):
Expand All @@ -698,14 +727,71 @@ class resource_type(TypeInfo):
id = "DashboardName"
name = "DashboardName"
cfn_type = "AWS::CloudWatch::Dashboard"
universal_taggable = object()
global_resource = True
permissions_augment = ("cloudwatch:ListTagsForResource",)

source_mapping = {
"describe": DescribeWithResourceTags,
"describe": DescribeDashboard,
}


@CloudWatchDashboard.action_registry.register('tag')
class DashboardTag(Tag):
"""Add tags to a cloudwatch dashboard

:example:

.. code-block:: yaml

policies:
- name: cloudwatch-dashboard-tag-owner
resource: aws.cloudwatch-dashboard
filters:
- "tag:Owner": absent
actions:
- type: tag
key: Owner
value: platform-team
"""

permissions = ("cloudwatch:TagResource",)

def process_resource_set(self, client, resource_set, tags):
for arn in self.manager.get_arns(resource_set):
self.manager.retry(client.tag_resource, ResourceARN=arn, Tags=tags)
Comment on lines +759 to +761

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dashboard deletion aborts tagging

If a dashboard is deleted after augmentation but before mutation, the new TagResource call propagates ResourceNotFound and aborts the entire policy run. The new read path already treats this race as benign, but both the tag action here and the remove-tag action at lines 784–787 leave it unhandled. These actions should skip a dashboard that disappeared and continue processing the rest.

Prompt To Fix With AI
This is a comment left during a code review.
Path: c7n/resources/cw.py
Line: 759-761

Comment:
**Dashboard deletion aborts tagging**

If a dashboard is deleted after augmentation but before mutation, the new `TagResource` call propagates `ResourceNotFound` and aborts the entire policy run. The new read path already treats this race as benign, but both the tag action here and the remove-tag action at lines 784–787 leave it unhandled. These actions should skip a dashboard that disappeared and continue processing the rest.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Handle ResourceNotFound/ResourceNotFoundException around both tag_resource and untag_resource calls. A dashboard deleted after augmentation currently aborts the action instead of skipping it and processing the remaining dashboards.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At c7n/resources/cw.py, line 761:

<comment>Handle `ResourceNotFound`/`ResourceNotFoundException` around both `tag_resource` and `untag_resource` calls. A dashboard deleted after augmentation currently aborts the action instead of skipping it and processing the remaining dashboards.</comment>

<file context>
@@ -698,14 +727,71 @@ class resource_type(TypeInfo):
+
+    def process_resource_set(self, client, resource_set, tags):
+        for arn in self.manager.get_arns(resource_set):
+            self.manager.retry(client.tag_resource, ResourceARN=arn, Tags=tags)
+
+
</file context>



@CloudWatchDashboard.action_registry.register('remove-tag')
class DashboardRemoveTag(RemoveTag):
"""Remove tags from a cloudwatch dashboard

:example:

.. code-block:: yaml

policies:
- name: cloudwatch-dashboard-remove-owner
resource: aws.cloudwatch-dashboard
filters:
- "tag:Owner": present
actions:
- type: remove-tag
tags: [Owner]
"""

permissions = ("cloudwatch:UntagResource",)

def process_resource_set(self, client, resource_set, tag_keys):
for arn in self.manager.get_arns(resource_set):
self.manager.retry(
client.untag_resource, ResourceARN=arn, TagKeys=tag_keys)


CloudWatchDashboard.filter_registry.register('marked-for-op', TagActionFilter)
CloudWatchDashboard.action_registry.register('mark-for-op', TagDelayedAction)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Policies using rename-tag now fail validation for cloudwatch-dashboard because the explicit native action registrations omit it. Add a dashboard-specific rename action that performs the native tag and untag calls, or preserve equivalent rename support.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At c7n/resources/cw.py, line 791:

<comment>Policies using `rename-tag` now fail validation for `cloudwatch-dashboard` because the explicit native action registrations omit it. Add a dashboard-specific rename action that performs the native tag and untag calls, or preserve equivalent rename support.</comment>

<file context>
@@ -698,14 +727,71 @@ class resource_type(TypeInfo):
+
+
+CloudWatchDashboard.filter_registry.register('marked-for-op', TagActionFilter)
+CloudWatchDashboard.action_registry.register('mark-for-op', TagDelayedAction)
+CloudWatchDashboard.action_registry.register('auto-tag-user', AutoTagUser)
+
</file context>

CloudWatchDashboard.action_registry.register('auto-tag-user', AutoTagUser)


@resources.register("destination")
class Destination(QueryResourceManager):
class resource_type(TypeInfo):
Expand Down
Loading