Imported from elC0mpa/aws-doctor (
AGENTS.md). Install upstream withnpx skills add elC0mpa/aws-doctor. Copyright stays with the author.
AI Agent Instructions for aws-doctor
This file provides instructions for AI coding agents working on this project. For human contributors, see CONTRIBUTING.md.
Project Overview
aws-doctor is a Go CLI tool that provides AWS cost analysis and waste detection. It acts as a free alternative to AWS Trusted Advisor.
Key Features
- Cost comparison between current and previous month
- 6-month trend analysis
- Waste detection (unused EIPs, EBS volumes, stopped instances, idle running EC2 instances, load balancers, idle NAT Gateways, idle SageMaker real-time inference endpoints, unused Secrets Manager secrets, etc.)
- Region-aware cost estimates:
service/pricingfetches rates from the AWS Pricing API (always via theus-east-1endpoint, filtered by the caller's region) at startup and caches them in memory. TheCalculate*helpers prefer the cached rate and fall back to the hardcoded us-east-1 defaults inconstants.go. Categories currently fetched from the Pricing API: EBS volumes, EIP, NAT Gateway, ALB/NLB, CLB, CloudWatch Logs, EC2 instances, RDS instances/storage/snapshots, SageMaker hosting (real-time inference) instances, and Secrets Manager. To add a new category, see the "Adding a Pricing Category" section in CONTRIBUTING.md. - Startup banner uses ANSI truecolor; title color switches to AmazonOrange when a blue background is detected (Windows console attributes or
COLORFGBGon Unix-like terminals), otherwise SkypeBlue. Override withAWS_DOCTOR_BANNER_COLOR(color name or ANSI code).
Quick Reference
# Build
go build ./...
# Test
go test ./...
# Run locally
go run . help
go run . cost
go run . waste
go run . trend
Architecture
aws-doctor/
|-- app.go # Main application entry, delegates to cmd
|-- cmd/ # Cobra CLI commands (root, waste, trend, etc.)
|-- model/ # Data structures and types
|-- service/
| |-- aws_config/ # AWS configuration loading
| |-- costexplorer/ # AWS Cost Explorer service
| |-- ec2/ # EC2 service (EIPs, EBS, instances)
| |-- elb/ # ELB service (load balancers)
| |-- orchestrator/ # Workflow coordination
| |-- output/ # Polymorphic Renderer interface (table/json/csv) & package-level printing helpers
| |-- sts/ # AWS STS service
| |-- update/ # Self-update workflow
|-- utils/ # Utility functions, table rendering
| |-- tui/ # Bubble Tea interactive UI components
|-- mocks/ # Test doubles
| |-- services/ # Internal service mocks (for orchestrator tests)
| |-- awsinterfaces/ # AWS SDK client mocks (for service tests)
|-- assets/ # Logos and images
|-- demo/ # Demo GIFs
Key Flows
app.godelegates execution tocmd.Execute().cmd/package defines Cobra commands (e.g.,cmd/waste.go). It uses domain-specific builders incmd/root.go(e.g.,buildWasteOrchestrator) to initialize AWS services and pass them into the appropriate orchestrator interface (WasteService,CostService, etc.).service/orchestratoris modularized into domain-specific files (service_waste.go,service_cost.go,service_system.go,service_trend.go). It executes workflow logic based on the configured model.Flags and streams background results via<-chan model.ScopeResult.service/outputdefines a polymorphicRendererinterface and acts as a factory (NewRenderer(format string)) to return the appropriate implementation (interactive TUI, static table, JSON, or CSV). It also provides package-level printing functions for system messages.service/updatehandles updates (invoked bycmd/update.go).
Service Pattern
Each service follows this pattern to enable Dependency Injection for testing:
types.go- Interface definitions (Service and AWS Client) and struct typesservice.go- Implementation
// types.go
// 1. Define interface for AWS client methods used
type SomeClientAPI interface {
SomeMethod(ctx context.Context, params *Input, optFns ...func(*Options)) (*Output, error)
}
type service struct {
client SomeClientAPI // Use interface, not concrete struct
}
// 2. Define service interface
type ServiceInterface interface {
Method(ctx context.Context) (Result, error)
}
// service.go
func NewService(awsconfig aws.Config) ServiceInterface {
client := someclient.NewFromConfig(awsconfig)
return &service{client: client}
}
Service Naming & Boundaries
- Method Naming: Always use the
Getprefix for service methods that retrieve information (e.g.,GetIdleNATGateways,GetRDSWaste). - Service Boundaries: Keep resources in their appropriate service. For example, NAT Gateway detection logic must reside in the
vpcservice, even if it uses the EC2 client under the hood. - Day Thresholds: Never hardcode day thresholds (idle days, stale days, etc.) inside individual services. These must be passed as parameters from the orchestrator and defined as constants in the
orchestratorservice. - Parameter Naming: Use
awsconfigas the parameter name foraws.Configin allNewServiceconstructors.
Git Workflow
Critical Rules
- Always target
developmentbranch for PRs, nevermain - Always rebase against upstream before pushing
- Fetch upstream frequently to stay current
Remote Setup
Contributors typically have:
origin- their forkupstream- the original repo (elC0mpa/aws-doctor)
Note: Some may use different names. Adjust commands accordingly.
# Sync with upstream before work
git fetch upstream
git checkout development
git reset --hard upstream/development
# Create feature branch
git checkout -b feat/feature-name upstream/development
# Before PR or when requested by maintainer
git fetch upstream
git rebase upstream/development
git push origin feat/feature-name --force
Code Guidelines
Imports
Use import aliases for AWS SDK packages to avoid conflicts:
import (
elb "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
elbtypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
)
Concurrency
Use errgroup for concurrent AWS API calls. For internal result collection, always prefer named internal structs over anonymous ones to improve readability and maintainability.
type result struct {
data Result
err error
}
g, ctx := errgroup.WithContext(ctx)
results := make([]result, count)
...
Pagination
Use AWS SDK v2 paginators for APIs that return paginated results:
paginator := elb.NewDescribeLoadBalancersPaginator(s.client, &elb.DescribeLoadBalancersInput{})
for paginator.HasMorePages() {
output, err := paginator.NextPage(ctx)
if err != nil {
return nil, err
}
results = append(results, output.Items...)
}
Error Handling
- Return errors to callers, don't log and continue
- Wrap errors with context using
fmt.Errorf("context: %w", err) - Check for nil pointers before dereferencing AWS response fields
Linting Compliance
The CI runs golangci-lint. Common issues to avoid:
- S1017: Use
strings.TrimPrefix(s, prefix)directly instead ofif strings.HasPrefix(s, prefix) { s = strings.TrimPrefix(s, prefix) } - Remove unused imports (the build will fail)
Testing
Current Approach
- Pure Unit Tests:
utils/*_test.go(no mocking required) - Service Unit Tests:
service/*package*/service_test.go(mocks AWS clients viamocks/awsinterfaces) - Orchestration Tests:
service/orchestrator/service_test.go(mocks internal services viamocks/services)
Test Style
Use table-driven tests:
func TestFunction(t *testing.T) {
tests := []struct {
name string
input Type
want Type
wantErr bool
}{
{"case_name", input, expected, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test
})
}
}
Common Tasks
Documentation Maintenance (Required)
Any change that affects behavior, flags, outputs, workflows, supported AWS resources, build/test steps, or architecture must be reflected in the documentation. Agents must update the relevant files as part of the same change:
AGENTS.mdfor agent guidance, architecture, workflows, and contribution rules.README.mdfor user-facing behavior, flags, features, and roadmap/checklists.CONTRIBUTING.mdandTESTING.mdfor contributor workflow and test guidance.docs/content/(Docs Site): For user-facing feature pages, configuration flags tables, and selective scanning arguments.
If a change makes documentation inaccurate or incomplete, treat the documentation update as mandatory and do it in the same patch/PR.
Docs Site Card Grids
The "Instant Infrastructure Audit" section on the home page (docs/content/_index.md / _index.es.md) and the "Categories of Detection" grid on the waste-detection index (docs/content/docs/waste-detection/_index.md / _index.es.md) must mirror each other: every waste-detection category card must appear in both grids, and vice versa. When adding or removing a waste-detection category, update both pages.
Rules for these grids:
- No duplicate links: Two cards in the same grid must never point to the same page or anchor. If a sub-feature (e.g., Lambda) lives on an existing category page (e.g., Compute), mention it in that category's subtitle instead of creating a separate card.
- One card per docs page: Each waste-detection docs page (
compute.md,databases.md,storage.md,networking.md,machine-learning.md,configuration.md, etc.) maps to exactly one card in each grid.
IAM Permissions Style
On the docs site, IAM permissions must be expressed using inline callouts or plain text with backtick-formatted action names (e.g., secretsmanager:ListSecrets). Never use JSON policy blocks to show required permissions. Follow the pattern used in each waste-detection category page:
{{</* callout type="info" */>}}
**Permissions Required**: `action:One`, `action:Two`.
{{</* /callout */>}}
Adding a New Waste Detection Type
- Model Type: Add the appropriate struct in
model/package (e.g.,model/ec2.goforKeyPairWasteInfo). - Client Interface: Define any new AWS client methods needed in the
*ClientAPIinterface (e.g.,service/ec2/types.go). - Client Mock: Update the corresponding mock in
mocks/awsinterfaces/to implement the new client method. - Service Method: Implement the logic in the service file (e.g.,
service/ec2/service.go). Use paginators for all AWS APIs that support them. - Service Interface: Add the new method to the
Serviceinterface intypes.go. - Service Mock: Update the service mock in
mocks/services/to include the new method. This is critical to avoidgo vetand build failures in orchestrator tests. - Analyzer Registration:
- Ensure the service implements the
analyzer.WasteAnalyzerinterface (AnalyzeandName). - If adding a completely new service, register it in the orchestrator builder (e.g.,
cmd/root.go,cmd/waste.go) via theregistry.Register()method. - If adding a new check inside an existing service, simply add the new method call to the concurrent
errgroupinside the service'sAnalyzemethod. No orchestrator changes needed!
- Ensure the service implements the
- Output Service:
- Update
model.RenderWasteInputinmodel/waste.goto include the new slice, and update itsMerge()method. - Update
RenderWastesignatures if necessary (usually only needed if you require new pricing dependencies).
- Update
- Utility Handlers:
- Add a display function in
utils/waste_table/waste_table.goand update theRenderScopeTableswitch statement for the new scope (if applicable). - Add a JSON output type in
model/output.goand updateutils/json_output/json_output.go.
- Add a display function in
- Test Compliance: Update all existing test calls in the service tests and
utilswhen function signatures change. Rungo test ./...frequently. - Documentation:
- Update the feature checklist and 'Selective scanning' command list in
README.md. - Add the new check to the 'Selective Scanning' table in
docs/content/docs/waste-detection/_index.mdand_index.es.md. - Create the dedicated documentation page for the category (e.g.,
security.mdandsecurity.es.md), but only if the category doesn't exist already. - Add the feature card to the Docs Site grids following the "Docs Site Card Grids" rules.
- Update the feature checklist and 'Selective scanning' command list in
- Validation: Run
go vet ./...andgolangci-lint runto ensure no regressions or interface mismatches were introduced.
Adding a New Command or Flag
The CLI uses the Cobra framework.
- New Command: Create a new file in
cmd/(e.g.,cmd/myfeature.go), define a*cobra.Command, and add it torootCmdin itsinit()function. - New Flag: Add persistent flags to
rootCmdincmd/root.gofor global flags, or local flags to specific commands. - Handle execution logic in the command's
RunEmethod (often by instantiating the orchestrator and callingorch.Orchestrate(flags)). - Update
model.Flagsstruct if you need to pass new flag states into the orchestrator. - Update
README.mddocumentation. - If a flag is added for waste detection, add it to the "Configuration Flags" table in
docs/content/docs/waste-detection/_index.mdand_index.es.md.
PR Checklist
Before submitting:
- Rebased against upstream
developmentbranch -
go build ./...succeeds -
go test ./...passes -
go vet ./...passes - New features have tests (for testable code)
- README.md updated if adding flags/features
- PR targets
developmentbranch (notmain)
After pushing:
- CI passes (build, lint, tests on Go 1.23 and 1.24)
- Address any golangci-lint warnings
PR Best Practices
- Keep PRs focused - one feature/fix per PR
- Maintainers may ask to split PRs - if a PR has parts with different dependencies, be prepared to split it
- Rebase when asked - maintainers may request rebasing after upstream changes
- CI must pass - fix any build, lint, or test failures before requesting review
Don't
- Don't modify production code solely to make it testable (discuss first)
- Don't add interfaces for mocking without maintainer approval
- Don't commit AWS credentials or sensitive data
- Don't target
mainbranch for PRs - Don't force push to shared branches after approval