Skip to content

✨ feat(infra): add enable_provisioner option to skip the provisioner Lambda - #443

Merged
sodre merged 4 commits into
mainfrom
feat/provisioner-toggle
Sep 10, 2026
Merged

✨ feat(infra): add enable_provisioner option to skip the provisioner Lambda#443
sodre merged 4 commits into
mainfrom
feat/provisioner-toggle

Conversation

@mrohr

@mrohr mrohr commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

StackOptions gains enable_provisioner (default True), mirroring enable_aggregator. The CloudFormation template already had an EnableProvisioner parameter with conditions guarding the provisioner Lambda, its role, and its log group — but nothing on the client side ever set it, so the provisioner was always deployed.

  • StackOptions.enable_provisioner + to_parameters() mapping to EnableProvisioner
  • StackManager / SyncStackManager parameter name mapping
  • RepositoryBuilder.enable_provisioner() (and sync counterpart)
  • CLI: --enable-provisioner/--no-provisioner on deploy, plus a Provisioner: enabled|disabled line in the deploy summary

Sync counterparts regenerated via hatch run generate-sync.

Bug fix: don't push Lambda code to functions CloudFormation never created

CloudFormation only creates the aggregator and provisioner functions when a role exists for them, but both deploy paths pushed code based on the enable_* flags alone. Under --no-iam / .create_iam(False) the functions are absent and the code push failed with ResourceNotFoundException, exiting 1 on an otherwise successful deploy.

  • CLI: --no-iam now auto-disables the provisioner the same way it auto-disables the aggregator
  • StackOptions gains deploys_aggregator_lambda / deploys_provisioner_lambda, mirroring the template's DeployAggregatorLambda / DeployProvisionerLambda conditions (aggregator's external-role escape hatch included), and Repository._ensure_infrastructure_internal() gates the code deploys on those — closing the same hole on the Repository.builder() path
Config Aggregator Lambda Provisioner Lambda
default deployed deployed
--no-provisioner deployed not created
--no-aggregator not created deployed
--no-iam not created not created
--no-iam + --aggregator-role-arn deployed not created

Visibility and error handling

Now that the provisioner is a real deployment choice, the CLI reports and explains its absence:

  • zae-limiter status gains a Provisioner: Enabled|Disabled|Unknown line in the Infrastructure section, next to the existing Aggregator: line. The template's ProvisionerFunctionName output is conditioned on DeployProvisionerLambda, so its presence in the stack outputs is exactly whether CloudFormation created the function — and status already calls describe_stacks for the role ARNs, so this costs no extra API call. When the stack outputs cannot be read at all it reports Unknown rather than Disabled, so an externally managed stack (Terraform/CDK) that omits the output is not misreported.
  • Bug fix (found during the real-AWS verification below): the existing Aggregator: line inferred state from the DynamoDB table's StreamSpecification, which the template enables unconditionally — so every stack reported Aggregator: Enabled, including one deployed with --no-aggregator. AggregatorFunctionName is conditioned on DeployAggregatorLambda exactly like ProvisionerFunctionName, so both states now come from the same describe_stacks call and share one Enabled/Disabled/Unknown renderer. The stream-spec inference is gone.
  • zae-limiter limits plan|apply|diff now exits 1 with an explanation when the provisioner Lambda is missing, instead of surfacing a raw botocore ResourceNotFoundException traceback.

Test plan

  • tests/unit/test_models.pyto_parameters() emits EnableProvisioner=false; deploys_*_lambda properties for default / --no-iam / external-role / disabled configs
  • tests/unit/test_repository_builder.py_ensure_infrastructure_internal() skips the disabled provisioner, skips both Lambdas without IAM, and still deploys the aggregator with an external role
  • tests/unit/test_cli.py::test_deploy_with_no_provisioner_flag--no-provisioner skips provisioner code deployment
  • tests/unit/test_limits_cli.py::test_invoke_provisioner_missing_function_exits_cleanly — clean exit 1, no traceback
  • tests/unit/test_cli.pystatus reports Enabled / Disabled / Unknown for both the aggregator and the provisioner (the status docstring sample output, which feeds docs/cli.md via mkdocs-click, was updated to match)
  • tests/unit/test_cli.py::test_status_aggregator_disabled_despite_table_stream — pins the regression: outputs present with no AggregatorFunctionNameDisabled
  • uv run pytest tests/unit/ -q → 2954 passed
  • ruff check, ruff format, mypy src/zae_limiter → clean
  • Sync generation → no drift
  • diff-cover vs origin/main → 100% patch coverage, 38 lines, 0 missing
  • Manual: zae-limiter deploy --name x --no-iam completes with exit 0 (previously exited 1)
  • Manual: zae-limiter deploy --name x --no-provisioner creates no {stack}-limits-provisioner function

Both manual items were verified against real AWS (account 733153035800, us-east-1) with real CloudFormation stacks, all since deleted:

Deploy Result
default exit 0; {stack}-limits-provisioner exists; status printed Provisioner: Enabled
--no-provisioner exit 0; provisioner absent; status printed Provisioner: Disabled; limits plan printed the new clean error (Error: Lambda function '...-limits-provisioner' not found. The limits provisioner is not deployed for this stack...) rather than a botocore traceback
--no-iam exit 0; provisioner absent — the regression this PR fixes, confirmed working

Caveat on the --no-iam case: it reaches exit 0 only when --permission-boundary and --role-name-format are also supplied. With either omitted the stack rolls back before the CLI ever reaches the provisioner step, due to a pre-existing bug on main that this PR neither touches nor causes: PermissionBoundaryArn and RoleNameFormat in src/zae_limiter/infra/cfn_template.yaml are exported unconditionally while both parameters default to '', and CloudFormation rejects empty export values (Cannot export output PermissionBoundaryArn. Exported values must not be empty or whitespace-only.). Introduced in 80a1a34 and now tracked in #445. The --no-iam fix in this PR is verified working.

🤖 Generated with Claude Code

https://claude.ai/code/session_014nPStTtHkW7zqj3X3wGpVg

@mrohr mrohr added this to the v1.3.0 milestone Sep 2, 2026
@mrohr mrohr added area/cli Command line interface area/infra CloudFormation, IAM, infrastructure labels Sep 2, 2026
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.93%. Comparing base (9485bcb) to head (3af38d7).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #443      +/-   ##
==========================================
+ Coverage   92.91%   92.93%   +0.02%     
==========================================
  Files          37       37              
  Lines        8324     8353      +29     
==========================================
+ Hits         7734     7763      +29     
  Misses        590      590              
Flag Coverage Δ
doctest 29.97% <15.78%> (-0.04%) ⬇️
e2e 43.74% <73.68%> (+0.08%) ⬆️
integration 53.72% <73.68%> (+0.05%) ⬆️
unit 92.82% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Matt Rohr and others added 2 commits September 9, 2026 17:51
…Lambda

StackOptions gained enable_provisioner (default: True), mirroring
enable_aggregator. It maps to the existing EnableProvisioner CloudFormation
parameter, which had conditions in the template but was never wired to a
client-side option.

- StackOptions.enable_provisioner + to_parameters() mapping
- RepositoryBuilder.enable_provisioner()
- Repository._ensure_infrastructure_internal() gates deploy_provisioner_code()
- CLI: --enable-provisioner/--no-provisioner on deploy

--no-iam now auto-disables the provisioner the same way it auto-disables the
aggregator. DeployProvisionerLambda requires DeployIAM, so the function was
never created under --no-iam, yet the CLI still tried to push code to it and
exited 1.

Sync counterparts regenerated.
The builder path pushed code to the aggregator and provisioner Lambdas
whenever their enable_* flags were set, but CloudFormation only creates
those functions when a role exists for them. Under create_iam=False the
functions are never created, so deploy_lambda_code() / deploy_provisioner_code()
failed with ResourceNotFoundException — the same bug the CLI just fixed
for --no-iam, reached through Repository.builder().create_iam(False).

StackOptions gains deploys_aggregator_lambda and deploys_provisioner_lambda,
which mirror the template's DeployAggregatorLambda / DeployProvisionerLambda
conditions (the aggregator's external-role escape hatch included), and
_ensure_infrastructure_internal now gates on those.

Also give `limits plan|apply|diff` a real error when the provisioner is not
deployed. That state is now reachable by design via --no-provisioner, and
it surfaced as a raw botocore ResourceNotFoundException traceback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nPStTtHkW7zqj3X3wGpVg
@sodre
sodre force-pushed the feat/provisioner-toggle branch from 5bb3dd3 to 80ce5f3 Compare September 9, 2026 22:18
@sodre
sodre marked this pull request as ready for review September 9, 2026 22:19

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 3af38d7 Previous: 9485bcb Ratio
tests/benchmark/test_localstack.py::TestLocalStackBenchmarks::test_acquire_release_localstack 18.103334101206823 iter/sec (stddev: 0.03938246977539952) 28.2083270139138 iter/sec (stddev: 0.007097106796083319) 1.56
tests/benchmark/test_localstack.py::TestCascadeOptimizationBenchmarks::test_cascade_with_batchgetitem_optimization 10.594873975464141 iter/sec (stddev: 0.10143684040737734) 29.740897984517193 iter/sec (stddev: 0.004872259368349679) 2.81
tests/benchmark/test_localstack.py::TestCascadeOptimizationBenchmarks::test_cascade_multiple_resources 9.95450375118606 iter/sec (stddev: 0.07499362296151371) 28.42705895758373 iter/sec (stddev: 0.004165152020627401) 2.86
tests/benchmark/test_localstack.py::TestLocalStackCascadeSpeculativeComparison::test_cascade_speculative_cache_cold_localstack 15.13881447788611 iter/sec (stddev: 0.06199454924695345) 28.30620601547383 iter/sec (stddev: 0.002496931856171016) 1.87
tests/benchmark/test_localstack.py::TestLocalStackCascadeSpeculativeComparison::test_cascade_speculative_cache_warm_localstack 18.036485534712767 iter/sec (stddev: 0.058482648534910656) 31.87679296055283 iter/sec (stddev: 0.003964527014694207) 1.77

This comment was automatically generated by workflow using github-action-benchmark.

`status` reported the aggregator but said nothing about the provisioner,
which `--no-provisioner` now makes a real choice worth seeing.

The template's ProvisionerFunctionName output is conditioned on
DeployProvisionerLambda, so its presence is exactly whether CloudFormation
created the function — and `status` already calls describe_stacks for the
role ARNs, so this costs no extra API call. Unreadable stack outputs report
Unknown rather than claiming Disabled, so an externally managed stack that
omits the output is not misreported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nPStTtHkW7zqj3X3wGpVg
…tream

`status` inferred the aggregator from the table's StreamSpecification, but
that is unconditional in the template — the stream is on whether or not the
aggregator was deployed — so every stack reported `Aggregator: Enabled`.
Caught on real AWS: a stack deployed with --no-aggregator still claimed the
aggregator was enabled.

AggregatorFunctionName is conditioned on DeployAggregatorLambda, exactly like
ProvisionerFunctionName, so both now come from the same describe_stacks call
and share the Enabled/Disabled/Unknown rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nPStTtHkW7zqj3X3wGpVg
@sodre
sodre merged commit 0ee3590 into main Sep 10, 2026
16 checks passed
@sodre
sodre deleted the feat/provisioner-toggle branch September 10, 2026 02:08
@sodre sodre modified the milestones: v1.3.0, v0.12.0 Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli Command line interface area/infra CloudFormation, IAM, infrastructure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants