Skip to content
Merged
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# https://docs.coderabbit.ai/reference/configuration
language: en-US
reviews:
tools:
golangci-lint:
enabled: true
config_file: .golangci.yml
31 changes: 17 additions & 14 deletions .cursor/rules/cli-account-selectors.mdc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
description: Account CLI selectors — explicit --account-id and --account-alias
globs: cli/internal/cmd/account*.go
globs: cli/internal/cmd/{tag,aws,account}*.go
alwaysApply: false
---

Expand All @@ -10,38 +10,41 @@ Account-targeting commands must be explicit. Do not autodetect alias vs account

## Required flags

- **`--account-id`** — 12-digit AWS account ID (never name this flag `--account`).
- **`--account-id`** — 12-digit AWS account ID (never name this flag `--account` on tag commands).
- **`--account-alias`** — registered finops alias.

Require **exactly one** of `--account-id` or `--account-alias`. Validate in command logic (not only Cobra mutual exclusion unless wired).

```go
// ✅ GOOD
accountTagsCmd.Flags().StringVar(&accountTagsAccountID, "account-id", "", "12-digit AWS account ID")
accountTagsCmd.Flags().StringVar(&accountTagsAlias, "account-alias", "", "Registered account alias")
// ✅ GOOD — tag commands
bindAWSAccountSelectorFlags(tagListCmd, awsAccountSelectorFlagRefs{
Payer: &accountTagsPayer, Alias: &accountTagsAlias, AccountID: &accountTagsAccountID,
}, "...")

// ❌ BADambiguous positional
Use: "list-tags <account>"
// ✅ GOODget-cost / snapshot / report create
bindAWSTargetFlags(accountGetCostCmd, awsTargetFlagRefs{Account: &costGetAccount, ...})

// ❌ BAD — inconsistent flag name
Flags().StringVar(&id, "account", "", "12-digit AWS account ID")
// ❌ BAD — ambiguous positional
Use: "list <account>"
```

## Examples

```bash
finops account list-tags --account-alias rh-control
finops account list-tags --account-id 111111111111 --payer rh-control
finops account update-tag --account-id 111111111111 --tag-key organization --tag-value "Hybrid Platform" --payer rh-control
finops tag list --account-alias rh-control
finops tag list --account-id 111111111111 --payer rh-control
finops tag update --account-id 111111111111 --tag-key organization --tag-value "Hybrid Platform" --payer rh-control
finops account get-cost --account-alias rh-control
finops aws list-ous --payer rh-control
```

## AWS Organizations account tags

- **List / add / update** tags: resolve target via `resolveAccountTagsTargetExplicit` (or shared helper with same contract).
- **Mutations** (`add-tag`, `update-tag`): use **payer** credentials. Support `--payer <payer-alias>` when the target is not mapped to a payer in config. Verify credential account is payer before calling Organizations write APIs.
- **Mutations** (`tag add`, `tag update`): use **payer** credentials. Support `--payer <payer-alias>` when the target is not mapped to a payer in config. Verify credential account is payer before calling Organizations write APIs.
- **Core** (`core/account`): use account-centric API names (`SetAccountTag`, `ListTagsForAccount`). Keep AWS SDK `TagResource` inside the Organizations client adapter only.

## Tests and docs

- Command tests set `--account-id` or `--account-alias` on package-level flag vars; do not pass positional account args.
- README and command `Long` help must use `--account-id`, not `--account`, for these commands.
- README and command `Long` help must use `--account-id`, not `--account`, for tag commands.
53 changes: 34 additions & 19 deletions .cursor/rules/cli-commands.mdc
Original file line number Diff line number Diff line change
@@ -1,42 +1,56 @@
---
description: finops CLI command naming — noun then verb
description: finops CLI command naming — singular nouns, config uses 3 levels
globs: cli/**/*
alwaysApply: false
---

# finops CLI commands

All user-facing commands follow **`finops <noun> <verb>`** (two words after `finops`).
Root commands use **singular nouns** (`account`, `snapshot`, `report`, `tag`, `aws`, `snowflake`, `config`).

## Structure

- **Noun** — resource or domain (`config`, `demo`, `cluster`, …). Implemented as a Cobra parent command attached to `rootCmd`.
- **Verb** — action on that noun (`login`, `verify`, `hello`, …). Implemented as a child of the noun command.
- **Args and flags** come after the verb: `finops account add aws 123456789012 --force`.
| Shape | Where | Example |
|-------|-------|---------|
| `config <sub-resource> <verb>` | Setup (3 levels) | `config account add`, `config default get`, `config oauth-client set` |
| `<noun> <verb>` | Core ops | `tag list`, `snapshot list`, `report create`, `snowflake query` |
| `<noun> <hyphenated-verb>` | Billing / AWS-specific | `account get-cost`, `aws list-ous` |

- **Args and flags** come after the verb: `finops config account add aws 123456789012 --force`.
- **`config account`** is registry setup; root **`account get-cost`** is billing.

## Examples

```bash
# ✅ GOOD
finops account add aws 123456789012
finops account list
# ✅ GOOD — setup (3-level config)
finops config account add aws 123456789012 --alias rh-control
finops config account list aws
finops config default set --name aws.auth-method --value profile
finops demo hello
finops config oauth-client set --client-id finops-tools-dataverse --client-secret "$SECRET"

# ✅ GOOD — core
finops account get-cost --account-alias rh-control
finops tag list --account-alias rh-control
finops aws list-ous --payer rh-control
finops report create costs --account-alias rh-control -o costs.html
finops snowflake query --alias sandbox "SELECT 1"

# ❌ BAD — verb or arg at root
finops hello
finops login aws
finops get-cost

# ❌ BAD — extra nesting (noun verb subcommand)
finops config aws login rh-control
# ❌ BAD — old paths (removed)
finops cost get
finops account add aws 123456789012
finops report generate costs
```

## Help groups

Root-level help (`finops --help`) groups noun commands into two sections:

- **Core** (`GroupID = "core"`) — day-to-day operations (`cost`, future domain nouns like `cluster`).
- **Setup & extra** (`GroupID = "setup"`) — local setup and utilities (`config`, `demo`).
- **Core** (`GroupID = "core"`) — day-to-day operations (`account`, `snapshot`, `report`, `tag`, `aws`, `snowflake`).
- **Setup** (`GroupID = "setup"`) — local setup (`config`).

Groups are defined on `rootCmd` in `cli/internal/cmd/root.go` via `AddGroup`. Every new root-level noun must set `GroupID` to a registered group ID.

Expand All @@ -46,13 +60,14 @@ Groups are defined on `rootCmd` in `cli/internal/cmd/root.go` via `AddGroup`. Ev
2. Add `cli/internal/cmd/<noun>_<verb>.go` with the verb child (`Use: "<verb> [args]"`).
3. Put business logic in `core/`; keep Cobra wiring thin. Never use `os/exec` in `core/` — external CLIs belong in `cli/` (see `core-no-shell.mdc`).
4. Terminal tables and colors go in `cli/internal/output/` (see `cli-pretty-output.mdc`).
5. Update README; CLI-owned hints belong in `cli/`, not `core/`.
5. Shared AWS target flags: use `bindAWSTargetFlags` / `bindAWSAccountSelectorFlags` from `aws_targets.go`.
6. Update README; CLI-owned hints belong in `cli/`, not `core/`.

## Naming

- Nouns and verbs are lowercase single words (kebab-case only if the domain name requires it).
- Prefer a dedicated verb command over a mode flag when behavior differs (e.g. `account add` vs a shared `login --check`).
- Command source files use the noun prefix: `report_generate.go` for `finops report generate`, not `reports_generate.go`.
- Root nouns are singular (`account`, not `accounts`).
- Nouns and verbs are lowercase; use kebab-case for multi-word verbs (`get-cost`, `list-ous`).
- Command source files use the noun prefix: `report_create.go` for `finops report create`, not `reports_create.go`.

## Scaling `cmd/`

Expand All @@ -62,4 +77,4 @@ Keep a flat `package cmd` until one of these triggers:
- Several verb files per noun exceed ~300 lines each, or
- Import cycles force shared logic out of `cmd`.

Then split into per-noun subpackages (`cli/internal/cmd/account/`, `cost/`, …), each exporting `NewCmd() *cobra.Command`, registered from `root.go`.
Then split into per-noun subpackages (`cli/internal/cmd/account/`, `report/`, …), each exporting `NewCmd() *cobra.Command`, registered from `root.go`.
2 changes: 2 additions & 0 deletions .cursor/rules/cli-list-formats.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ For user-facing list-style commands (for example: `list`, `list-tags`, `get` ret
2. Wire output through a formatter switch (`pretty-print`, `json`, `csv`).
3. Keep JSON/CSV raw (no ANSI, no pretty-table-only formatting assumptions).
4. Mention all supported formats in command help text/examples.
5. Provide `--output` / `-o` via `addOutputFlag` and write with `resolveCommandOutput`.

## Example

```go
cmd.Flags().StringVar(&listFormat, "format", string(output.FormatPrettyPrint), "Output format: pretty-print, json, csv")
addOutputFlag(cmd, &listOutput)
```
2 changes: 1 addition & 1 deletion .cursor/rules/cli-pretty-output.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Use `cli/internal/format` (`FormatAmount`, `FormatMoney`) for all human-readable

## Machine-readable formats

When a command needs JSON/CSV, follow `cost.go`: `ParseFormat`, `Write*Result(w, format, data)`, default `pretty-print`. Pretty path always goes through `*_pretty.go`. Do not add thousands separators to JSON/CSV.
When a command needs JSON/CSV, follow `account_get_cost.go`: `ParseFormat`, `Write*Result(w, format, data)`, default `pretty-print`. Pretty path always goes through `*_pretty.go`. Do not add thousands separators to JSON/CSV.

## Tests

Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: golangci-lint

on:
pull_request:
push:
branches:
- main
- master

jobs:
detect-modules:
runs-on: ubuntu-latest
outputs:
modules: ${{ steps.set-modules.outputs.modules }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version: "1.25"
- id: set-modules
run: echo "modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')" >> $GITHUB_OUTPUT

golangci-lint:
needs: detect-modules
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
strategy:
fail-fast: false
matrix:
modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version: "1.25"
- name: golangci-lint (${{ matrix.modules }})
uses: golangci/golangci-lint-action@v9
with:
version: v2.12
working-directory: ${{ matrix.modules }}
only-new-issues: true
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
/cache/
*.exe

# Local Snowflake OAuth credentials and tokens (use finops config snowflake oauth set)
# Local Snowflake OAuth credentials and tokens (use finops config oauth-client set)
**/snowflake-oauth.yaml
**/snowflake-tokens.yaml
8 changes: 8 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "2"

run:
timeout: 5m
go: "1.25"

linters:
default: standard
19 changes: 17 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
.PHONY: test build run clean build-backend test-backend podman-build podman-push \
.PHONY: test lint lint-install build run clean build-backend test-backend podman-build podman-push \
openshift-apply openshift-restart openshift-refresh

GOPATH_BIN := $(shell go env GOPATH)/bin
GOLANGCI_LINT := $(GOPATH_BIN)/golangci-lint
GOLANGCI_VERSION := v2.12.2
GOLANGCI_PACKAGES := $(shell go list -f '{{.Dir}}/...' -m)

test:
go test ./core/... ./cli/... ./backend/...

lint-install:
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)

$(GOLANGCI_LINT):
$(MAKE) lint-install

# golangci-lint cannot use ./... at the go.work root; lint each workspace module.
lint: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run $(GOLANGCI_PACKAGES)

build:
go build -o bin/finops ./cli/cmd/finops

Expand Down Expand Up @@ -39,7 +54,7 @@ openshift-restart:
openshift-refresh: podman-push openshift-apply openshift-restart

run: build
./bin/finops demo hello
./bin/finops --help

clean:
rm -rf bin dist
Expand Down
Loading
Loading