From c704bdb513d06f197923c9854c03ae9464148664 Mon Sep 17 00:00:00 2001 From: Andrea Cappelletti Date: Fri, 17 Jul 2026 18:19:31 -0400 Subject: [PATCH] Warn when a resource is detected but not priced A parsed resource that matched no pricing dimension (e.g. an unmapped Cloud SQL tier) was only surfaced behind --show-skipped, so by default it vanished from the estimate and users could assume it was free (issue #48, second point). Emit a concise warning to stderr by default whenever resources are skipped, keeping the full per-resource detail behind --show-skipped and stdout clean for --format json/csv. --- cmd/c3x/estimate.go | 25 +++++++++++++++++++------ cmd/c3x/estimate_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/cmd/c3x/estimate.go b/cmd/c3x/estimate.go index 8af3ed3..a3585b3 100644 --- a/cmd/c3x/estimate.go +++ b/cmd/c3x/estimate.go @@ -223,14 +223,27 @@ func runEstimate( return err } - if showSkipped && len(est.Skipped) > 0 { + if len(est.Skipped) > 0 { // Print to stderr so machine-readable formats (--format json, // --format csv) on stdout stay parseable. - fmt.Fprintf(cmd.ErrOrStderr(), - "\n%d resource(s) were detected but not priced:\n", len(est.Skipped)) - for _, s := range est.Skipped { - fmt.Fprintf(cmd.ErrOrStderr(), " - %s.%s: %s\n", - s.Resource.Kind, s.Resource.Name, s.Reason) + if showSkipped { + fmt.Fprintf(cmd.ErrOrStderr(), + "\n%d resource(s) were detected but not priced:\n", len(est.Skipped)) + for _, s := range est.Skipped { + fmt.Fprintf(cmd.ErrOrStderr(), " - %s.%s: %s\n", + s.Resource.Kind, s.Resource.Name, s.Reason) + } + } else { + // Warn by default: a resource that couldn't be priced must + // not be silently dropped, or users assume it's free. + verb := "resources were" + if len(est.Skipped) == 1 { + verb = "resource was" + } + fmt.Fprintf(cmd.ErrOrStderr(), + "\nwarning: %d %s detected but not priced and excluded from the "+ + "total; re-run with --show-skipped for details.\n", + len(est.Skipped), verb) } } diff --git a/cmd/c3x/estimate_test.go b/cmd/c3x/estimate_test.go index 0c85ffa..d7e71a1 100644 --- a/cmd/c3x/estimate_test.go +++ b/cmd/c3x/estimate_test.go @@ -61,6 +61,46 @@ func TestEstimateAgainstRealTerraform(t *testing.T) { } } +// TestEstimateWarnsOnUnpricedResource is the regression guard for #48: +// a resource that is parsed but matches no pricing dimension must be +// surfaced with a warning by default, not silently dropped from the +// output (which would let users assume it's free). +func TestEstimateWarnsOnUnpricedResource(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + dir := t.TempDir() + // db-n1-standard-1 is a legacy Cloud SQL tier with no catalog + // mapping, so every compute dimension guards out and the instance + // cannot be priced. The aws_instance keeps the estimate non-empty. + if err := os.WriteFile(filepath.Join(dir, "main.tf"), []byte(` + provider "aws" { region = "us-east-1" } + resource "aws_instance" "web" { + ami = "ami-x" + instance_type = "t3.micro" + } + resource "google_sql_database_instance" "legacy" { + database_version = "POSTGRES_15" + region = "us-central1" + settings { + tier = "db-n1-standard-1" + } + } + `), 0o644); err != nil { + t.Fatal(err) + } + + cmd := newRootCmd() + out := &bytes.Buffer{} + cmd.SetOut(out) + cmd.SetErr(out) + cmd.SetArgs([]string{"estimate", "--path", dir}) + if err := cmd.Execute(); err != nil { + t.Fatalf("estimate failed: %v", err) + } + if got := out.String(); !strings.Contains(got, "not priced") { + t.Errorf("expected an unpriced-resource warning by default, got:\n%s", got) + } +} + // TestEstimateAcceptsVarFlag wires up --var=name=value end-to-end. func TestEstimateAcceptsVarFlag(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir())