Project Goals
Build a CLI tool that queries a cloud API, aggregates resource information, and outputs formatted reports in multiple formats.
Your program should:
- Connect to a REST API (we'll use the GitHub API — free, no account needed for public data)
- Fetch and paginate through resources
- Aggregate data: count by category, compute statistics
- Output reports in table, JSON, and CSV formats
- Handle errors, rate limiting, and timeouts gracefully
Why This Project
This is the kind of tool every infra team builds internally. Whether it's querying AWS, GCP, or an internal inventory system, the pattern is always: connect → fetch → aggregate → report. This project exercises HTTP clients, JSON handling, pagination, and data aggregation.
Usage
# Report on a GitHub org's repos
cloudreport repos --org kubernetes --format table
# Report with sorting
cloudreport repos --org hashicorp --sort stars --limit 20
# JSON output for piping to jq
cloudreport repos --org prometheus --format json | jq '.repos[].name'
# CSV for spreadsheets
cloudreport repos --org grafana --format csv > repos.csv
Expected Output
=== Repository Report: kubernetes ===
Total repositories: 20 (showing top 20 by stars)
NAME STARS FORKS LANG UPDATED
kubernetes 105841 38420 Go 2024-01-15
minikube 28102 4891 Go 2024-01-14
ingress-nginx 16421 8102 Go 2024-01-15
dashboard 13542 4021 TypeScript 2024-01-13
kops 15520 5610 Go 2024-01-12
Summary:
Total stars: 179,426
Languages: Go (14), TypeScript (3), Shell (2), Python (1)
Updated in last 7 days: 18
Requirements
Core
- HTTP client with configurable timeout (default 30s) and User-Agent header.
- Pagination: The GitHub API returns 30 items per page. Follow
Linkheader or use?page=N&per_page=100to get all results. - Rate limiting: Check the
X-RateLimit-Remainingheader. If approaching the limit (< 10), warn the user and slow down. - Data model: Parse API responses into typed structs:
type Repo struct { Name string `json:"name"` Stars int `json:"stargazers_count"` Forks int `json:"forks_count"` Language string `json:"language"` UpdatedAt time.Time `json:"updated_at"` Description string `json:"description"` } - Aggregation: Total stars, repos per language, recently updated count.
- Output formats: Table (aligned columns), JSON (pretty-printed), CSV (with headers).
CLI Flags
--org— GitHub organization to query (required)--format— output format:table(default),json,csv--sort— sort by:stars(default),name,updated,forks--limit— max repos to display (default: 20, 0 = all)--timeout— HTTP timeout in seconds (default: 30)
API Endpoints
GET https://api.github.com/orgs/{org}/repos?per_page=100&page=1&sort=updated
Response headers to check:
X-RateLimit-Remaining— requests left in the current windowX-RateLimit-Reset— Unix timestamp when the window resetsLink— pagination links (next, last)
Suggested Structure
cloudreport/
├── main.go ← CLI entry point
├── client.go ← HTTP client, pagination, rate limit handling
├── client_test.go ← Tests using httptest.NewServer
├── models.go ← Repo struct, aggregation types
├── aggregate.go ← Sorting, grouping, statistics
├── output.go ← Table, JSON, CSV formatters
└── output_test.go ← Format output tests
Hints
Suggested approach:
- Start by fetching a single page from the GitHub API and parsing the JSON
- Add pagination — fetch all pages into a
[]Repo- Add sorting and limiting
- Build the table formatter first (most useful for debugging)
- Add JSON and CSV formatters
- Add rate limit checking
- Add the aggregation summary
Pagination Helper
func (c *Client) fetchAllRepos(org string) ([]Repo, error) {
var all []Repo
page := 1
for {
repos, hasNext, err := c.fetchReposPage(org, page)
if err != nil {
return nil, err
}
all = append(all, repos...)
if !hasNext {
break
}
page++
}
return all, nil
}
Table Formatting
// Compute column widths from data, then use fmt.Sprintf with padding
fmt.Sprintf("%-*s %-*d %-*d %-*s", nameWidth, name, 8, stars, 8, forks, langWidth, lang)
Testing
Use httptest.NewServer to mock the GitHub API:
func TestFetchRepos(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]Repo{
{Name: "test-repo", Stars: 42, Language: "Go"},
})
}))
defer server.Close()
client := NewClient(server.URL, 10*time.Second)
repos, err := client.fetchAllRepos("test-org")
// assert...
}
Stretch Goals
- Caching: Cache API responses to disk with TTL (avoid hitting rate limits during development)
- Diff mode: Compare current report with a saved previous report, show what changed
- GitHub token: Accept
GITHUB_TOKENenv var for authenticated requests (higher rate limits) - Multiple orgs: Accept multiple
--orgflags and merge results - Sparklines: Show star history using Unicode block characters
Skills Used: HTTP clients, JSON parsing, pagination, struct methods, sorting (sort.Slice), string formatting, multiple output formats, CLI flags, error handling, test mocking with httptest.