Skip to content

Commit

Permalink
feat(cli): Create a generic Config struct (#9)
Browse files Browse the repository at this point in the history
Signed-off-by: pokom <[email protected]>
  • Loading branch information
Pokom authored Nov 30, 2023
1 parent a2392df commit 04122bb
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 24 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ go run cmd/exporter/exporter.go -provider gcp -project-id=ops-tools-1203

# GCP - prod - with custom bucket projects

go run cmd/exporter/exporter.go -provider gcp -project-id=ops-tools-1203 -gcp.bucket-projects=grafanalabs-global,ops-tools-1203
go run cmd/exporter/exporter.go -provider gcp -project-id=ops-tools-1203 -gcp.bucket-projects=grafanalabs-global -gcp.bucket-projects=ops-tools-1203

# GCP - dev
go run cmd/exporter/exporter.go -provider gcp -project-id=grafanalabs-dev
Expand All @@ -49,7 +49,7 @@ go run cmd/exporter/exporter.go -provider aws -aws.profile workloads-prod
> [!Note]
> GCP Only: you can specify the services to collect cost metrics on.
> `gcs` is collected by default.
> To collect GKE, append any of the gcp commands with `-gcp.services=gke,gcs`
> To collect GKE, append any of the gcp commands with `-gcp.services=gke -gcp.services=gcs`
## Architecture

Expand Down
31 changes: 31 additions & 0 deletions cmd/exporter/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package config

import (
"time"
)

type Config struct {
Provider string
ProjectID string
Providers struct {
AWS struct {
Profiles StringSliceFlag
Region string
Services StringSliceFlag
}
GCP struct {
DefaultGCSDiscount int
Projects StringSliceFlag
Region string
Services StringSliceFlag
}
}
Collector struct {
ScrapeInterval time.Duration
}

Server struct {
Address string
Path string
}
}
14 changes: 14 additions & 0 deletions cmd/exporter/config/string_slice_flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package config

import "strings"

type StringSliceFlag []string

func (f *StringSliceFlag) String() string {
return strings.Join(*f, ",")
}

func (f *StringSliceFlag) Set(value string) error {
*f = append(*f, value)
return nil
}
71 changes: 71 additions & 0 deletions cmd/exporter/config/string_slice_flag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package config

import (
"flag"
"testing"
)

func TestStringSliceFlag_Set(t *testing.T) {
tests := map[string]struct {
values []string
exp int
}{
"empty": {
values: []string{},
exp: 0,
},
"single": {
values: []string{"-test", "test1"},
exp: 1,
},
"multiple": {
values: []string{"-test", "test1", "-test", "test2"},
exp: 2,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
var ssf StringSliceFlag
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.Var(&ssf, "test", "test")
if err := fs.Parse(test.values); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if exp, got := test.exp, len(ssf); exp != got {
t.Fatalf("expected %d, got %d", exp, got)
}
})
}
}

func TestStringSliceFlag_String(t *testing.T) {
tests := map[string]struct {
values []string
exp string
}{
"empty": {
values: []string{},
exp: "",
}, "single": {
values: []string{"test1"},
exp: "test1",
}, "multiple": {
values: []string{"test1", "test2"},
exp: "test1,test2",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var ssf StringSliceFlag
for _, v := range test.values {
if err := ssf.Set(v); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if exp, got := test.exp, ssf.String(); exp != got {
t.Fatalf("expected %q, got %q", exp, got)
}
})
}
}
52 changes: 30 additions & 22 deletions cmd/exporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,30 @@ import (

dto "github.com/prometheus/client_model/go"

"github.com/grafana/cloudcost-exporter/cmd/exporter/config"
"github.com/grafana/cloudcost-exporter/pkg/aws"
"github.com/grafana/cloudcost-exporter/pkg/collector"
"github.com/grafana/cloudcost-exporter/pkg/google"
)

func ProviderFlags(fs *flag.FlagSet, awsProfiles, gcpProjects, awsServices, gcpServices *config.StringSliceFlag) {
fs.Var(awsProfiles, "aws.profile", "AWS profile(s).")
// TODO: RENAME THIS TO JUST PROJECTS
fs.Var(gcpProjects, "gcp.bucket-projects", "GCP project(s).")
fs.Var(awsServices, "aws.services", "AWS service(s).")
fs.Var(gcpServices, "gcp.services", "GCP service(s).")
}

func main() {
var cfg config.Config
ProviderFlags(flag.CommandLine, &cfg.Providers.AWS.Profiles, &cfg.Providers.GCP.Projects, &cfg.Providers.AWS.Services, &cfg.Providers.GCP.Services)
provider := flag.String("provider", "aws", "AWS or GCP")
scrapeInterval := flag.Duration("scrape-interval", 1*time.Hour, "Scrape interval")
awsRegion := flag.String("aws.region", "", "AWS region")
awsProfile := flag.String("aws.profile", "", "AWS profile")
projectId := flag.String("project-id", "ops-tools-1203", "Project ID to target.")
gcpDefaultDiscount := flag.Int("gcp.default-discount", 19, "GCP default discount")
// TODO: Deprecate this flag in favor of `gcp.projects`
gcpProjects := flag.String("gcp.bucket-projects", "", "GCP projects to fetch resources from. Must be a list of comma-separated project IDs. If no value is passed it, defaults to the project ID passed via --project-id.")
gcpServices := flag.String("gcp.services", "GCS", "GCP services to scrape. Must be a list of comma-separated service names.")
awsServices := flag.String("aws.services", "S3", "AWS services to scrape. Must be a list of comma-separated service names.")
flag.DurationVar(&cfg.Collector.ScrapeInterval, "scrape-interval", 1*time.Hour, "Scrape interval")
flag.StringVar(&cfg.Providers.AWS.Region, "aws.region", "", "AWS region")
flag.StringVar(&cfg.ProjectID, "project-id", "ops-tools-1203", "Project ID to target.")
flag.StringVar(&cfg.Server.Address, "server.address", ":8080", "Default address for the server to listen on.")
flag.StringVar(&cfg.Server.Path, "server.path", "/metrics", "Default path for the server to listen on.")
flag.IntVar(&cfg.Providers.GCP.DefaultGCSDiscount, "gcp.default-discount", 19, "GCP default discount")
flag.Parse()

log.Print("Version ", version.Info())
Expand All @@ -43,20 +51,20 @@ func main() {
switch *provider {
case "aws":
csp, err = aws.NewAWS(&aws.Config{
Region: *awsRegion,
Profile: *awsProfile,
ScrapeInterval: *scrapeInterval,
Services: strings.Split(*awsServices, ","),
Region: cfg.Providers.AWS.Region,
Profile: cfg.Providers.AWS.Profiles.String(),
ScrapeInterval: cfg.Collector.ScrapeInterval,
Services: strings.Split(cfg.Providers.AWS.Services.String(), ","),
})

case "gcp":
csp, err = google.NewGCP(&google.Config{
ProjectId: *projectId,
Region: *awsRegion,
Projects: *gcpProjects,
DefaultDiscount: *gcpDefaultDiscount,
ScrapeInterval: *scrapeInterval,
Services: strings.Split(*gcpServices, ","),
ProjectId: cfg.ProjectID,
Region: cfg.Providers.GCP.Region,
Projects: cfg.Providers.GCP.Projects.String(),
DefaultDiscount: cfg.Providers.GCP.DefaultGCSDiscount,
ScrapeInterval: cfg.Collector.ScrapeInterval,
Services: strings.Split(cfg.Providers.GCP.Services.String(), ","),
})
default:
err = fmt.Errorf("unknown provider")
Expand All @@ -74,12 +82,12 @@ func main() {
}

// Collect http server for prometheus
http.Handle("/metrics", promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{
http.Handle(cfg.Server.Path, promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{
EnableOpenMetrics: true,
}))

// TODO: Add proper shutdown sequence here. IE, listen for sigint and sigterm and shutdown gracefully.
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Printf("Listening on %s:%s", cfg.Server.Address, cfg.Server.Path)
if err = http.ListenAndServe(cfg.Server.Address, nil); err != nil {
log.Printf("Error listening and serving: %s", err)
os.Exit(1)
}
Expand Down

0 comments on commit 04122bb

Please sign in to comment.