Skip to content
Merged
Changes from all commits
Commits
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
157 changes: 156 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ CUDly is a comprehensive CLI tool for managing cloud cost commitments across AWS

## Supported Cloud Providers & Services

### AWS Services (Production Ready)
### AWS Services

| Service | Commitment Type | Description |
|---------|----------------|-------------|
Expand Down Expand Up @@ -48,6 +48,23 @@ CUDly is a comprehensive CLI tool for managing cloud cost commitments across AWS
| Memorystore | Committed Use Discounts |
| Cloud Storage | Committed Use Discounts |

### AWS CLI Support Matrix

**Tested** means the service has been exercised end-to-end with real AWS
accounts and validated in production workloads. **Experimental** means the
implementation exists and is functional, but needs real-world validation --
contributions and testers are very welcome.

| AWS Service | CLI Flag | Status |
| ----------- | -------- | ------ |
| Amazon RDS | `rds` | **Tested** |
| Amazon ElastiCache | `elasticache` | **Tested** |
| Amazon EC2 (Reserved Instances) | `ec2` | Experimental (seeking testers) |
| Amazon OpenSearch | `opensearch` | Experimental (seeking testers) |
| Amazon Redshift | `redshift` | Experimental (seeking testers) |
| Amazon MemoryDB | `memorydb` | Experimental (seeking testers) |
| Savings Plans (Compute, EC2 Instance, SageMaker, Database) | `savingsplans` | Experimental (seeking testers) |

## Installation

### From Source
Expand Down Expand Up @@ -427,6 +444,144 @@ CUDly/
- **Plugin architecture** - Services registered and discovered at runtime
- **Safety-first** - Multiple layers of protection against unintended purchases

## Web Interface (Experimental)

> **Note: The web GUI is experimental.** It is under active development and
> has not been validated at scale. Use the CLI for production workloads.

In addition to the CLI, this branch ships a browser-based dashboard. The same
Go binary that runs the CLI also acts as the application server: it serves the
pre-built TypeScript/Webpack frontend as static files (controlled by the
`STATIC_DIR` environment variable) and exposes a REST API at `/api/`. There is
no separate web server process.

### What the dashboard provides

| Area | What you can do |
| ---- | --------------- |
| **Dashboard** | Summary of active commitments, upcoming expirations, and savings trends |
| **Recommendations** | Browse and refresh commitment recommendations; trigger purchases from the UI |
| **Purchase plans** | Create, approve, pause, resume, and delete planned-purchase workflows; view execution history |
| **History** | Full purchase history with analytics and cost-breakdown views |
| **Inventory & Coverage** | List active commitments across accounts; view per-provider, per-service coverage breakdown |
| **RI Exchange** | AWS Convertible RI exchange: reshape recommendations, quote, and execute exchanges |
| **Settings** | Application configuration, cloud account credentials, user/group management, API keys |

### Capabilities and limitations

The web interface is part of the `feat/multicloud-web-frontend` branch and is
not yet merged to `main`. The dashboard is operational for AWS workloads; Azure
and GCP support in the web UI follows the same maturity as the CLI providers
(both are experimental). Specifically:

- **AWS**: recommendations, purchases, RI exchange, inventory, and coverage
views are all wired and backed by real AWS APIs (Cost Explorer, EC2, RDS,
etc.). This is the primary tested path.
- **Azure**: reservation recommendations and purchases are implemented in the
API handlers (see `internal/api/handler_recommendations.go`,
`providers/azure/`), but Azure support is experimental. The RI Exchange
feature covers Azure Convertible RIs as a distinct code path.

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify RI Exchange implementation scope across providers

# Search for Azure-specific RI exchange implementation
rg -n -C3 --type=go 'exchange.*azure|azure.*exchange' -i

# Check RI exchange handler and routing
rg -n -C3 --type=go 'func.*[Ee]xchange.*Handler|HandleRIExchange|ri.exchange'

# Look for provider-specific exchange logic
ast-grep --pattern $'func $_($$$ provider $$$) {
  $$$
  exchange
  $$$
}'

Repository: LeanerCloud/CUDly

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show README section around the referenced lines
if [ -f README.md ]; then
  echo "---- README.md:450-480 ----"
  sed -n '450,480p' README.md | nl -ba -w2 -s':'
else
  echo "README.md not found"
fi

# 2) Find where the 'targets AWS' wording exists (in README or elsewhere)
echo "---- Searching for 'targets AWS Convertible EC2' ----"
rg -n -S "targets AWS Convertible EC2|target(s)? AWS Convertible EC2|Convertible EC2 Reserved Instances only" . || true

# 3) Inspect RI exchange handler around target-offerings / execute / quote
echo "---- handler_ri_exchange.go: locate target-offerings and quote/execute ----"
rg -n "target-offerings|listTargetOfferings|execute|quote|Offerings|Convertible" internal/api/handler_ri_exchange.go

echo "---- Show relevant sections of internal/api/handler_ri_exchange.go ----"
# Show around listTargetOfferingsHandler implementation (usually near listTargetOfferings in file)
rg -n "listTargetOfferings|TargetOfferingsResponse" internal/api/handler_ri_exchange.go | head -n 20
# Show chunks around match lines
for ln in $(rg -n "listTargetOfferings|TargetOfferingsResponse" internal/api/handler_ri_exchange.go | awk -F: '{print $1}' | head -n 10); do
  start=$((ln-40)); end=$((ln+80));
  echo "---- internal/api/handler_ri_exchange.go:${start}-${end} (around ${ln}) ----"
  sed -n "${start},${end}p" internal/api/handler_ri_exchange.go | nl -ba -w2 -s':'
done

# 4) Inspect provider-specific execute/quote logic for offerings targets AWS-only
echo "---- Search for AWS-only offering logic in handler_ri_exchange.go and providers ----"
rg -n -S "AWS Convertible|ConvertibleRI|ec2|Offerings|target offer" internal/api/handler_ri_exchange.go providers || true

Repository: LeanerCloud/CUDly

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show README section around the referenced lines
if [ -f README.md ]; then
  echo "---- README.md:450-480 ----"
  sed -n '450,480p' README.md | nl -ba -w2 -s':'
else
  echo "README.md not found"
fi

# 2) Find where the 'targets AWS' wording exists (in README or elsewhere)
echo "---- Searching for 'targets AWS Convertible EC2' ----"
rg -n -S "targets AWS Convertible EC2|target(s)? AWS Convertible EC2|Convertible EC2 Reserved Instances only" . || true

# 3) Inspect RI exchange handler around target-offerings / execute / quote
echo "---- handler_ri_exchange.go: locate target-offerings and quote/execute ----"
rg -n "target-offerings|listTargetOfferings|execute|quote|Offerings|Convertible" internal/api/handler_ri_exchange.go

echo "---- Show relevant sections of internal/api/handler_ri_exchange.go ----"
rg -n "listTargetOfferings|TargetOfferingsResponse" internal/api/handler_ri_exchange.go | head -n 20
for ln in $(rg -n "listTargetOfferings|TargetOfferingsResponse" internal/api/handler_ri_exchange.go | awk -F: '{print $1}' | head -n 10); do
  start=$((ln-40)); end=$((ln+80));
  echo "---- internal/api/handler_ri_exchange.go:${start}-${end} (around ${ln}) ----"
  sed -n "${start},${end}p" internal/api/handler_ri_exchange.go | nl -ba -w2 -s':'
done

# 4) Inspect provider-specific execute/quote logic for offerings targets AWS-only
echo "---- Search for AWS-only offering logic in handler_ri_exchange.go and providers ----"
rg -n -S "AWS Convertible|ConvertibleRI|ec2|Offerings|target offer" internal/api/handler_ri_exchange.go providers || true

Repository: LeanerCloud/CUDly

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Helper to print numbered lines without `nl`
print_range() {
  local file="$1" start="$2" end="$3"
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d:%s\n", NR, $0}' "$file"
}

# 1) README context around 450-480
if [ -f README.md ]; then
  echo "---- README.md:450-480 ----"
  print_range README.md 450 480
else
  echo "README.md not found"
fi

# 2) Find exact wording in README (and nowhere else)
echo "---- Searching for the exact wording / close variants ----"
rg -n -S "targets AWS Convertible EC2 Reserved Instances only|Convertible EC2 Reserved Instances only|targets AWS Convertible EC2" README.md || true

# 3) Inspect handler_ri_exchange.go around listTargetOfferings and execute/quote
echo "---- handler_ri_exchange.go matches (target-offerings/execute/quote) ----"
rg -n "listTargetOfferings|target-offerings|execute|quote|TargetOfferings" internal/api/handler_ri_exchange.go

echo "---- handler_ri_exchange.go excerpts around listTargetOfferings ----"
# Print a small window around the first few matches
for ln in $(rg -n "listTargetOfferings|target-offerings|TargetOfferings" internal/api/handler_ri_exchange.go | awk -F: '{print $1}' | head -n 5); do
  start=$((ln-60)); end=$((ln+140));
  echo "---- internal/api/handler_ri_exchange.go:${start}-${end} (around ${ln}) ----"
  sed -n "${start},${end}p" internal/api/handler_ri_exchange.go
done

# 4) Confirm Azure source vs target/offering usage
echo "---- Search for Azure reservations / exchangeable Azure RIs usage ----"
rg -n "azure.*RIs|ExchangeableAzureRIs|listExchangeableAzureRIs|ListExchangeableReservations" internal/api/handler_ri_exchange.go providers/azure/services/compute/exchange.go

Repository: LeanerCloud/CUDly

Length of output: 50373


Clarify Azure RI Exchange scope/terminology in README
Lines 463 and 466-467 aren’t contradictory in function: the Azure-specific RI Exchange code path provides exchangeable Azure VM reservation candidates, while the exchange “target offerings” are AWS Convertible EC2 Reserved Instances only. Adjust line 463 to say “exchangeable Azure VM reservations” (or similar) instead of “Azure Convertible RIs” to avoid implying Azure uses the same “convertible RI” concept as AWS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 463, Replace the ambiguous phrase "Azure Convertible RIs"
in the README with a clearer term such as "exchangeable Azure VM reservations"
to reflect that the Azure-specific RI Exchange code path yields Azure VM
reservation candidates, while the exchange "target offerings" remain AWS
Convertible EC2 Reserved Instances; update the sentence containing "Azure
Convertible RIs" (and any nearby phrasing referencing the Azure code path) to
use the new wording so it doesn't imply Azure supports AWS-style "convertible
RI" semantics.

- **GCP**: GCP commitment recommendations and purchases are experimental. The
handler routing exists, but end-to-end coverage is limited compared to AWS.
- The RI Exchange feature currently targets AWS Convertible EC2 Reserved
Instances only.
- Multi-account support (AWS Organizations) is implemented; Azure/GCP
multi-account federation is in progress.

### Deployment (self-hosted via Terraform)

CUDly is **self-hosted only**. You deploy it into your own cloud account using
the Terraform configurations under `terraform/environments/`. The Terraform
modules build and push a Docker container image, provision the database,
secrets, and networking, and deploy the application to one of the supported
runtimes.

| Cloud | Runtime | Terraform environment |
| ----- | ------- | --------------------- |
| AWS | Lambda (default) or Fargate (ECS) | `terraform/environments/aws/` |
| GCP | Cloud Run | `terraform/environments/gcp/` |
| Azure | Container Apps | `terraform/environments/azure/` |

#### Prerequisites

- Terraform >= 1.6.0
- Docker with buildx
- Go 1.25+
- Cloud CLI authenticated: `aws`, `gcloud`, or `az`

#### Quick deploy (using the helper script)

```bash
# AWS dev
./scripts/tf-deploy.sh aws dev

# GCP dev
./scripts/tf-deploy.sh gcp dev

# Azure dev
./scripts/tf-deploy.sh azure dev
```

#### Manual Terraform (AWS example)

```bash
cd terraform/environments/aws
cp dev.tfvars.example dev.tfvars # edit with your values
terraform init -backend-config=backends/dev.tfbackend
terraform plan -var-file=dev.tfvars
terraform apply -var-file=dev.tfvars
```

See [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for the full deployment guide,
including Azure and GCP details, CDN/CloudFront configuration, remote state
backends, and CI/CD integration.

**Key `tfvars` fields**

| Variable | Purpose |
| -------- | ------- |
| `admin_email` | Email address for the initial administrator account |
| `admin_password` | Initial admin password (leave unset to auto-generate and store in Secrets Manager) |
| `compute_platform` | AWS only: `"lambda"` (default, scale-to-zero) or `"fargate"` (always-warm ECS) |

The Terraform apply also handles Docker image build/push and database
migrations automatically on each apply.

### Accessing the dashboard

After `terraform apply` completes, retrieve the application URL from the
Terraform outputs:

```bash
# AWS Lambda
terraform -chdir=terraform/environments/aws output lambda_function_url

# AWS Fargate (ALB)
terraform -chdir=terraform/environments/aws output fargate_api_url

# GCP Cloud Run
terraform -chdir=terraform/environments/gcp output cloud_run_service_url

# Azure Container Apps
terraform -chdir=terraform/environments/azure output container_app_url
```

Open that URL in your browser. On a fresh deployment the login page includes a
one-time **"Set up admin"** step. Provide the `admin_email` you configured in
`tfvars` and either the password you set or the one retrieved from Secrets
Manager:

```bash
# Retrieve the auto-generated admin password (AWS)
aws secretsmanager get-secret-value \
--secret-id "$(terraform -chdir=terraform/environments/aws output -raw admin_password_secret_name)" \
--query SecretString --output text
```

After the admin account is created, log in with that email and password. You
can then add more users, configure cloud account credentials, and begin using
the dashboard.

## Development

### Prerequisites
Expand Down
Loading