Skip to content

Commit 9a54588

Browse files
Align status command with platform tagging model
- Filter costs by `team` tag instead of non-existent `service` tag - Filter ECS services by `{team}-` prefix instead of loose string match - Read team/service from app.yaml when run from a service repo - Replace --project flag with --team and --service flags
1 parent 32b0857 commit 9a54588

File tree

4 files changed

+74
-49
lines changed

4 files changed

+74
-49
lines changed

cmd/status.go

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,92 +3,115 @@ package cmd
33
import (
44
"context"
55
"fmt"
6-
"os/exec"
6+
"os"
77
"strings"
88

99
"github.com/javaBin/javabin-cli/internal/aws"
10-
"github.com/javaBin/javabin-cli/internal/config"
1110
"github.com/spf13/cobra"
11+
"gopkg.in/yaml.v3"
1212
)
1313

14-
var projectFlag string
14+
var teamFlag string
15+
var serviceFlag string
1516

1617
var statusCmd = &cobra.Command{
1718
Use: "status",
18-
Short: "Show project status (costs, services, deployments)",
19-
RunE: runStatus,
19+
Short: "Show team costs and ECS service status",
20+
Long: `Show month-to-date AWS costs for a team and ECS service status.
21+
22+
Flags --team and --service override auto-detection. If run from a directory
23+
with an app.yaml, team and service name are read from it automatically.`,
24+
RunE: runStatus,
2025
}
2126

2227
func init() {
23-
statusCmd.Flags().StringVar(&projectFlag, "project", "", "Project name (inferred from git remote if not set)")
28+
statusCmd.Flags().StringVar(&teamFlag, "team", "", "Team name (reads from app.yaml if not set)")
29+
statusCmd.Flags().StringVar(&serviceFlag, "service", "", "Service name (reads from app.yaml if not set)")
30+
}
31+
32+
type appYaml struct {
33+
Name string `yaml:"name"`
34+
Team string `yaml:"team"`
35+
}
36+
37+
func readAppYaml() *appYaml {
38+
data, err := os.ReadFile("app.yaml")
39+
if err != nil {
40+
return nil
41+
}
42+
var app appYaml
43+
if err := yaml.Unmarshal(data, &app); err != nil {
44+
return nil
45+
}
46+
return &app
2447
}
2548

2649
func runStatus(cmd *cobra.Command, args []string) error {
27-
project := projectFlag
28-
if project == "" {
29-
project = inferProject()
50+
team := teamFlag
51+
service := serviceFlag
52+
53+
if team == "" || service == "" {
54+
if app := readAppYaml(); app != nil {
55+
if team == "" {
56+
team = app.Team
57+
}
58+
if service == "" {
59+
service = app.Name
60+
}
61+
}
62+
}
63+
64+
if team == "" {
65+
return fmt.Errorf("could not determine team — use --team flag or run from a directory with app.yaml")
3066
}
31-
if project == "" {
32-
return fmt.Errorf("could not infer project name — use --project flag or run from a javaBin repo")
67+
68+
fmt.Printf("Team: %s\n", team)
69+
if service != "" {
70+
fmt.Printf("Service: %s\n", service)
3371
}
72+
fmt.Println()
3473

35-
fmt.Printf("Project: %s\n\n", project)
3674
ctx := context.Background()
37-
3875
cfg, err := aws.LoadConfig(ctx)
3976
if err != nil {
4077
return fmt.Errorf("AWS credentials not configured: %w", err)
4178
}
4279

4380
// Cost this month
4481
fmt.Println("--- Costs (month-to-date) ---")
45-
cost, err := aws.GetMonthlyCost(ctx, cfg, project)
82+
cost, err := aws.GetTeamMonthlyCost(ctx, cfg, team)
4683
if err != nil {
4784
fmt.Printf(" Could not fetch costs: %v\n", err)
4885
} else {
49-
fmt.Printf(" Spend: $%.2f\n", cost)
86+
fmt.Printf(" Team spend: $%.2f\n", cost)
5087
}
5188

5289
// ECS services
5390
fmt.Println("\n--- ECS Services ---")
5491
services, err := aws.ListServices(ctx, cfg, "javabin-platform")
5592
if err != nil {
5693
fmt.Printf(" Could not list services: %v\n", err)
57-
} else if len(services) == 0 {
58-
fmt.Println(" No running services")
5994
} else {
95+
prefix := team + "-"
96+
found := false
6097
for _, svc := range services {
61-
if strings.Contains(svc.Name, project) {
62-
fmt.Printf(" %s running=%d desired=%d\n", svc.Name, svc.RunningCount, svc.DesiredCount)
98+
if !strings.HasPrefix(svc.Name, prefix) {
99+
continue
100+
}
101+
if service != "" && svc.Name != prefix+service {
102+
continue
103+
}
104+
fmt.Printf(" %s running=%d desired=%d\n", svc.Name, svc.RunningCount, svc.DesiredCount)
105+
found = true
106+
}
107+
if !found {
108+
if service != "" {
109+
fmt.Printf(" No services matching %s%s\n", prefix, service)
110+
} else {
111+
fmt.Printf(" No services matching %s*\n", prefix)
63112
}
64113
}
65114
}
66115

67-
// TODO: Last 5 deployments (requires ECS describe-services with deployments)
68-
// TODO: Untagged resources (requires Config or resource group tagging API)
69-
70-
_ = config.EnsureConfigDir()
71116
return nil
72117
}
73-
74-
func inferProject() string {
75-
out, err := exec.Command("git", "remote", "get-url", "origin").Output()
76-
if err != nil {
77-
return ""
78-
}
79-
url := strings.TrimSpace(string(out))
80-
// Handle both HTTPS and SSH URLs
81-
// https://github.com/javaBin/moresleep.git -> moresleep
82-
// git@github.com:javaBin/moresleep.git -> moresleep
83-
for _, prefix := range []string{
84-
"https://github.com/javaBin/",
85-
"git@github.com:javaBin/",
86-
} {
87-
if strings.HasPrefix(url, prefix) {
88-
name := strings.TrimPrefix(url, prefix)
89-
name = strings.TrimSuffix(name, ".git")
90-
return name
91-
}
92-
}
93-
return ""
94-
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ require (
2525
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2626
github.com/jmespath/go-jmespath v0.4.0 // indirect
2727
github.com/spf13/pflag v1.0.5 // indirect
28+
gopkg.in/yaml.v3 v3.0.1 // indirect
2829
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
4848
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4949
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
5050
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
51+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5152
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/aws/aws.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ func GetCallerIdentity(ctx context.Context, cfg aws.Config) (*CallerIdentity, er
3939
}, nil
4040
}
4141

42-
// GetMonthlyCost returns month-to-date cost for a project tag.
43-
func GetMonthlyCost(ctx context.Context, cfg aws.Config, project string) (float64, error) {
42+
// GetTeamMonthlyCost returns month-to-date cost for a team tag.
43+
func GetTeamMonthlyCost(ctx context.Context, cfg aws.Config, team string) (float64, error) {
4444
client := costexplorer.NewFromConfig(cfg, func(o *costexplorer.Options) {
4545
o.Region = "us-east-1" // Cost Explorer is global
4646
})
@@ -57,8 +57,8 @@ func GetMonthlyCost(ctx context.Context, cfg aws.Config, project string) (float6
5757
Metrics: []string{"UnblendedCost"},
5858
Filter: &cetypes.Expression{
5959
Tags: &cetypes.TagValues{
60-
Key: aws.String("service"),
61-
Values: []string{project},
60+
Key: aws.String("team"),
61+
Values: []string{team},
6262
},
6363
},
6464
})

0 commit comments

Comments
 (0)