Repository navigation
fix(iac): fire delayed purchases by default; collect AWS recommendations at 02:00 UTC - #1580
Conversation
…ons at 02:00 UTC Owner decision D33 (LeanerCloud#712): the fire_scheduled_purchases sweep is now on by default on every platform. enable_fire_scheduled_purchases_schedule defaults to true in the aws, azure and gcp environment roots and in the Lambda, Fargate, Cloud Run, GKE, Container Apps and AKS modules, and the CloudFormation EnableFireScheduledPurchasesSchedule parameter defaults to "true". The sweep only executes purchases already approved for a later time, and the 24h due-row age cap (LeanerCloud#1549) refuses and alerts on anything older, so the first run cannot buy a stale backlog. The process sweep and every other opt-in toggle stay off. The Go policy test now treats fire as default-on (it would flag a committed AWS tfvars that turns it off), and the module terraform tests expect the fire job by default and cover the opt-out. Owner decision D32 (LeanerCloud#756): the AWS recommendation_schedule default moves from rate(1 day) to cron(0 2 * * ? *), 02:00 UTC like GCP and Azure, in the env root, the Lambda and Fargate modules, the github-*.tfvars and the example tfvars. A rate() schedule has no known phase, so the freshness endpoint's next_collection_at was always null on AWS. A new test reads every default and committed value and checks the API's NextCollectionAt returns the next 02:00 UTC for each. Refs LeanerCloud#712 Refs LeanerCloud#756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault-aws-schedule
LeanerCloud#1556 assumes a bastion account's target role with the bastion's own credentials and no longer falls back to the CUDly host's, so a target role that trusts the host instead of the bastion now gets AccessDenied until its trust policy names the bastion. Refs LeanerCloud#712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LeanerCloud#1432 (D29) deletes cloudformation/stacks/CUDly/ and scheduled_task_cfn_test.go, so the CloudFormation edits in this branch would only cause modify/delete conflicts. Restore both files to main and drop CloudFormation from the CHANGELOG entry. The fire sweep's default changes on the Terraform platforms only. Until LeanerCloud#1432 lands, TestCloudFormationSchedulesEveryRequiredTask and the "opt-in task on by default" negative in that file fail on this branch alone, because the policy now treats fire as default-on and the retiring template still defaults it off; LeanerCloud#1432 removes both tests. Refs LeanerCloud#712 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TestRecommendationScheduleDefaultsReportNextCollection borrowed tfStringDefaultRe from scheduled_task_cfn_test.go, which LeanerCloud#1432 deletes; the combined tree would not compile. Declare a local copy instead. Refs LeanerCloud#712 Refs LeanerCloud#756 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rM2. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rM1. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
…fault-aws-schedule
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked: I confirmed the head is d99c879; origin/main is one unrelated commit ahead (#1581) and merge-tree is clean. #1577 (CFN retirement) is in the head, so the merge-order caveat no longer applies. Traced every writer of Body-stale:
Reviewer: rO2. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rO1. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Addendum to my rO1 review above (same head, same verdict). I skimmed the earlier reviews after writing mine and add two points that came from them: Blocking:
Non-blocking:
Attacked: the brief's claim that only Body-stale: as in my review above. Reviewer: rO1. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 2 approving independent review(s) on |
Implements two acting-owner decisions (IAmJSD, 2026-10-28) on top of the sweep scheduling that #1549, #1550 and #1573 already landed for #712, and adds one release notice the coordinator asked for.
Refs #712
Refs #756
After this merges, #712 can be closed. Its last open item, turning the fire sweep on by default, is done here, and every platform already schedules every task. The process sweep staying opt-in is the owner's decision, not leftover work. One gap stays documented in
knownGaps: the GKE module deployed from the GCP environment root gets noenable_scheduled_tasks/app_url, so it schedules nothing until someone picks the URL Cloud Scheduler should call. That needs its own decision and is not part of this issue's remaining scope.D33: fire_scheduled_purchases sweep on by default (#712)
Root cause.
enable_fire_scheduled_purchases_scheduledefaulted tofalseeverywhere. A delayed auto-approved purchase (statusscheduled, withscheduled_execution_atset) never ran unless an operator had turned the sweep on, and the GCP/Azure github environments had not.Fix.
enable_fire_scheduled_purchases_schedulenow defaults totrueinterraform/environments/{aws,azure,gcp}/variables.tfand in each module that declares it: AWS Lambda, AWS Fargate, GCP Cloud Run, GCP GKE, Azure Container Apps and Azure AKS. The descriptions now say why turning it on is safe. Since fix(purchase): skip scheduled purchases due more than 24h ago in the fire and process sweeps #1549 the server refuses any row due more than 24h ago (SCHEDULED_PURCHASE_MAX_OVERDUE): the row is marked failed withno_submission, operators get the purchase-failed alert, and a stale backlog is never bought.false. No other opt-in default changed (send_notifications,cleanup,ladder_run,ri_exchange).main.tfrule header, thesweep_taskslocals in Cloud Run, GKE, Container Apps and AKS, and theErrDueRowTooOlddoc comment.terraform/modules/computetakefire_scheduled_purchasesout ofoptInRequired. The env/module default checks and the committed-AWS-tfvars check now treat it as default-on. The negative fixtures that used fire as their opt-in example now use process. A new negative covers "a tfvars file switches fire off", plus an explicit assertion that fire is default-on and process is opt-in.terraform testfiles (Lambda, Fargate, Cloud Run, Container Apps, AKS) now expect the fire job in the default plan. Each one also gets an opt-out run (enable_fire_scheduled_purchases_schedule = falseremoves the rule, target and permission, the job and its OIDC audience, or the workflow), or an existing all-off run now covers fire.What each environment does on its next apply. No committed tfvars (the
github-*.tfvars,dev.tfvars.exampleorterraform/profiles/*) setsenable_fire_scheduled_purchases_scheduleorenable_process_scheduled_purchases_schedule, so the new default applies everywhere:compute_platform = "lambda"): creates the<stack>-fire-scheduled-purchasesEventBridge rule (rate(15 minutes)), its target and its Lambda permission. A Fargate deploy creates the rule, the ECS target and the invoker role/policy instead.cloud-run,enable_scheduled_tasks = true): creates the<service>-fireCloud Scheduler job (*/15 * * * *) and adds its audience toSCHEDULED_TASK_OIDC_AUDIENCE, which rolls a new Cloud Run revision.container-apps,enable_scheduled_tasks = true): creates the fire Logic App workflow (every 15 minutes) with its user-assigned identity, Key Vault Secrets User assignment and actions.D32: AWS collection at 02:00 UTC (#756)
Root cause. The AWS
recommendation_scheduledefault wasrate(1 day). A rate schedule's phase depends on when the rule was created, which the API cannot see, soGET /api/recommendations/freshnessalways returnednext_collection_at: nullon AWS (PR #1567).Fix. The default is now
cron(0 2 * * ? *)(02:00 UTC, the same as GCP and Azure) in:terraform/environments/aws/variables.tfterraform/environments/aws/github-{dev,staging,prod}.tfvarsenvironments/aws/dev.tfvars.examplehadrate(24 hours), andprofiles/aws/*.tfvars.example), so copies of them don't bring back the nullThe backend needed no change:
schedexpralready parses EventBridge cron, andinternal/api'sTestFreshnessNextCollectionAtalready coverscron(0 2 * * ? *)→2026-10-28T02:00:00Z. The new testTestRecommendationScheduleDefaultsReportNextCollection(terraform/modules/compute/recommendation_schedule_env_test.go) ties the IaC to the API. It reads the default in the AWS, GCP and Azure env roots and the Lambda and Fargate modules, plus every committed AWS tfvars value. It runs each one throughapi.NewHandler(...).NextCollectionAtand requires the next 02:00 UTC. It also fails if agithub-*.tfvarsstops setting the value.On the next AWS apply, the collection rule's
schedule_expressionchanges in place. TheRECOMMENDATION_SCHEDULEenv var updates the Lambda function in place; on Fargate it registers a new task definition and rolls the service.D27 notice: bastion target roles (#1556)
The coordinator asked for a CHANGELOG notice after #1556 (Refs #263) merged: CUDly now assumes a bastion account's target role with the bastion's own credentials and no longer falls back to the CUDly host's. A bastion account whose target role trusts the CUDly host instead of the bastion now gets AccessDenied until the bastion is added as the trusted principal in that role's trust policy. The notice is under
## [Unreleased]→### Notices, linking #1556. This branch merged origin/main (6e2033c) to pick it up.CHANGELOG
Verification
All of this ran locally on Linux. It is unit, fixture and mocked-provider evidence; nothing touched a real cloud account and nothing was planned or applied against one.
Fail-before: I reverse-applied this branch's IaC changes and kept the new tests, with
go test -count=1 ./terraform/modules/compute/:Same revert, mocked
terraform test -filter=<file>: thedefaultsrun fails in Lambdasweeps, Fargatesweeps, Cloud Runscheduled_tasks, Container Appsscheduled_tasksand AKSscheduled_tasks.Pass-after, at the branch head:
go build ./...,go vet ./...,go vet -tags integration ./...: cleango test -count=1 ./terraform/modules/compute/: everything passes except two tests inscheduled_task_cfn_test.go, which fix(cfn): CloudFormation CUDly stack can't start the current server (no DB_HOST; still provisions DynamoDB) #1432 deletes. This branch must merge after fix(cfn): CloudFormation CUDly stack can't start the current server (no DB_HOST; still provisions DynamoDB) #1432.TestCloudFormationSchedulesEveryRequiredTaskand the "opt-in task on by default" case ofTestCloudFormationScheduleCheckNegativesfail on this branch alone, because the policy now treats fire as default-on while the retiring template, left at main's content, still defaults it off. On a local merge of this branch withfix/issue-1432-retire-cfn-stack(9cb7de8), the merge is clean (no conflicts),go build ./...passes, andgo test -count=1 ./terraform/modules/compute/...is all ok.go test -count=1 ./internal/purchase/: okgo test -count=1 ./internal/api/ -run 'Freshness|NewHandlerParsesRecommendationSchedule': okgo test ./internal/mocks/ -count=1: okgolangci-lint run --allow-parallel-runners ./terraform/modules/compute/... ./internal/purchase/...(v2.10.1): 0 issuesgocyclo -over 10 -ignore "_test\.go" internal/purchase/due_age_cap.go: nothing over 10terraform fmt -recursive -check terraform/: cleanterraform init -backend=false && terraform validateinterraform/environments/{aws,azure,gcp}andterraform/modules/compute/{aws/lambda,aws/fargate,gcp/cloud-run,gcp/gke,azure/container-apps,azure/aks}: all validterraform test(every provider mocked) in each touched module with tests: Lambda 16/16, Fargate 4/4, Cloud Run 10/10, Container Apps 14/14, AKS 14/14 passed. GKE has no tftest files.Combined-tree verification. Tested on a local, never-pushed tree:
queue/platform@40dbbdaf= origin/main6e2033cdplus:Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers.go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....--build-tags integration.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, one package at a time):internal/api,internal/database/postgres/migrations,internal/purchaseandinternal/serverpass.internal/confighad one failure, which comes from the fix(marketplace): close RI Marketplace poller gaps (pending rows with a listing id, superseded listings, deployment coverage) #1494 branch (a mid-claim listing gets settled); that branch is being fixed and isn't opened yet.scripts/test-git-secrets-allowlist.shpasses.Out of scope (deliberately not changed)
internal/config/defaults.gostill storesscheduling.collect_schedule = "rate(1 day)"as a settings default. It does not drive any scheduler.app_url). This is documented inknownGapsand needs its own decision.Frontend follow-up for the UI owner
No UI change is needed for this PR. The remaining #756 item (showing
next_collection_aton the empty-state card) is still with the UI owner, and AWS now returns a real value for it.Review notes
Maintainer: please mirror #712's triage labels onto this PR:
priority/p1,severity/high,urgency/this-sprint,impact/many,effort/m,type/bug,triaged(#756 is p2/medium, so the higher #712 set covers it).🤖 Generated with Claude Code