Skip to content

smarterr CloudWatch Integration: Declarative, Config-Driven Error Handling #43121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
129 changes: 129 additions & 0 deletions .ci/semgrep/smarterr/enforce.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
rules:
- id: go-no-sdkdiag-appendfromerr
languages: [go]
message: Use smerr.Append(ctx, diags, err) instead of sdkdiag.AppendFromErr.
severity: ERROR
pattern: sdkdiag.AppendFromErr($DIAGS, $ERR)
fix: smerr.Append(ctx, $DIAGS, $ERR)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-sdkdiag-appenderrorf
languages: [go]
message: Use smerr.Append(ctx, diags, err, smerr.ID, ...) instead of sdkdiag.AppendErrorf.
severity: ERROR
patterns:
- pattern: sdkdiag.AppendErrorf($DIAGS, $FMT, $ID, $ERR)
- pattern: sdkdiag.AppendErrorf($DIAGS, $FMT, $ERR)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-create-appenddiagerror
languages: [go]
message: Use smerr.Append(ctx, diags, err, smerr.ID, ...) instead of create.AppendDiagError.
severity: ERROR
pattern: create.AppendDiagError($DIAGS, ...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-diagnostics-adderror
languages: [go]
message: Use smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, ...) instead of Diagnostics.AddError.
severity: ERROR
patterns:
- pattern: $RESP.Diagnostics.AddError($MSG, $ERR)
- pattern: $RESP.Diagnostics.AddError(fmt.Sprintf($FMT, ...), $ERR)
- pattern: $RESP.Diagnostics.AddError($MSG, $ERR.Error())
- pattern-not-inside: smerr.AddError(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-create-adderror
languages: [go]
message: Use smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, ...) instead of create.AddError.
severity: ERROR
pattern: create.AddError(&$RESP.Diagnostics, ...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-direct-diag-adderror
languages: [go]
message: Use smerr.AddError instead of resp.Diagnostics.AddError (migrate to smarterr/smerr).
severity: ERROR
patterns:
- pattern: $RESP.Diagnostics.AddError($MSG, $ERR)
- pattern-not-inside: smerr.AddError(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-direct-diag-appenderrorf
languages: [go]
message: Use smerr.Append or smerr.EnrichAppend instead of diag.AppendErrorf (migrate to smarterr/smerr).
severity: ERROR
pattern: diag.AppendErrorf(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-direct-diag-appendfromerr
languages: [go]
message: Use smerr.Append or smerr.EnrichAppend instead of diag.AppendFromErr (migrate to smarterr/smerr).
severity: ERROR
pattern: diag.AppendFromErr(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-direct-diag-append
languages: [go]
message: Use smerr.EnrichAppend instead of resp.Diagnostics.Append (migrate to smarterr/smerr).
severity: ERROR
patterns:
- pattern: $RESP.Diagnostics.Append(...)
- pattern-not-inside: smerr.EnrichAppend(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-bare-return-err
languages: [go]
message: Return errors wrapped with smarterr.NewError (migrate to smarterr).
severity: ERROR
patterns:
- pattern: |
return nil, $ERR
- pattern-not-inside: |
return nil, smarterr.NewError(...)
paths:
include:
- internal/service/cloudwatch/

- id: go-no-bare-assertsinglevalueresult
languages: [go]
message: Wrap tfresource.AssertSingleValueResult with smarterr.Assert (migrate to smarterr).
severity: ERROR
patterns:
- pattern: |
return tfresource.AssertSingleValueResult(...)
- pattern-not-inside: smarterr.Assert(tfresource.AssertSingleValueResult(...))
paths:
include:
- internal/service/cloudwatch/

- id: go-no-bare-empty-result-error
languages: [go]
message: Wrap tfresource.NewEmptyResultError with smarterr.NewError (migrate to smarterr).
severity: ERROR
patterns:
- pattern: |
return nil, tfresource.NewEmptyResultError(...)
- pattern-not-inside: smarterr.NewError(tfresource.NewEmptyResultError(...))
paths:
include:
- internal/service/cloudwatch/
48 changes: 48 additions & 0 deletions .github/workflows/smarterr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: smarterr config check

on:
push:
branches:
- main
- "release/**"
pull_request:
paths:
- "**/*.go"
- .github/workflows/smarterr_check.yml

## NOTE: !!!
## When changing these workflows, ensure that the following is updated:
## - Documentation: docs/continuous-integration.md
## - Documentation: docs/makefile-cheat-sheet.md
## - Makefile: ./GNUmakefile

jobs:
modern:
name: Check smarterr config
runs-on: custom-ubuntu-22.04-medium
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: go.mod
# See also: https://github.com/actions/setup-go/issues/54
- name: go env
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_ENV
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
continue-on-error: true
timeout-minutes: 2
with:
path: ${{ env.GOCACHE }}
key: ${{ runner.os }}-GOCACHE-${{ hashFiles('go.sum') }}-${{ hashFiles('internal/**') }}
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
continue-on-error: true
timeout-minutes: 2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-pkg-mod-${{ hashFiles('go.sum') }}
# long-running test
- uses: YakDriver/check-smarterr-config@v0.3.0 # v0.3.0
name: Check smarterr config
with:
base-dir: './internal'
1 change: 1 addition & 0 deletions .teamcity/scripts/provider_tests/acceptance_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ TF_ACC=1 go test \
./internal/sdkv2/... \
./internal/semver/... \
./internal/slices/... \
./internal/smerr/... \
./internal/sweep/... \
./internal/tags/... \
./internal/tfresource/... \
Expand Down
1 change: 1 addition & 0 deletions .teamcity/scripts/provider_tests/unit_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ go test \
./internal/sdkv2/... \
./internal/semver/... \
./internal/slices/... \
./internal/smerr/... \
./internal/sweep/... \
./internal/tags/... \
./internal/tfresource/... \
Expand Down
166 changes: 166 additions & 0 deletions docs/smarterr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# smarterr Migration Guide for AI and Human Contributors

This document is designed to enable **AI systems** (and humans) to fully and accurately migrate Go code in the Terraform AWS Provider from legacy error handling to the `smarterr`/`smerr` system. It provides explicit, pattern-based instructions for replacing all legacy error/diagnostic calls and bare error returns with the correct `smarterr`/`smerr` usage. **Follow these rules exactly for every migration.**

---

## What is smarterr?

`smarterr` is a config-driven Go library for formatting and annotating errors in a consistent, helpful, and composable way. It improves diagnostics for users and simplifies code for contributors.

- **Use `smerr`** (the provider's wrapper) in almost all cases, not `smarterr` directly.
- `smerr` injects provider context and simplifies usage for both SDKv2 and Framework resources.

---

## Migration Rules: Legacy → smarterr/smerr

### 1. Replace All Legacy Diagnostic/Error Calls

**For each of the following legacy calls, replace as shown:**

| Legacy Call | Replace With |
|---|---|
| `sdkdiag.AppendFromErr(diags, err)` | `smerr.Append(ctx, diags, err, smerr.ID, ...)` |
| `sdkdiag.AppendErrorf(diags, ..., err)` | `smerr.Append(ctx, diags, err, smerr.ID, ...)` |
| `create.AppendDiagError(diags, ..., err)` | `smerr.Append(ctx, diags, err, smerr.ID, ...)` |
| `response.Diagnostics.AddError(..., err.Error())` | `smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, ...)` |
| `resp.Diagnostics.AddError(..., err.Error())` | `smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, ...)` |
| `create.AddError(&response.Diagnostics, ..., err)` | `smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, ...)` |
| `return nil, err` | `return nil, smarterr.NewError(err)` |
| `return nil, &retry.NotFoundError{ LastError: err, LastRequest: ..., }` | `return nil, smarterr.NewError(&retry.NotFoundError{ LastError: err, LastRequest: ..., })` |
| `return nil, tfresource.NewEmptyResultError(...)` | `return nil, smarterr.NewError(tfresource.NewEmptyResultError(...))` |
| `return tfresource.AssertSingleValueResult(...)` | `return smarterr.Assert(tfresource.AssertSingleValueResult(...))` |

**Examples:**

- `sdkdiag.AppendFromErr(diags, err)` → `smerr.Append(ctx, diags, err)`
- `sdkdiag.AppendErrorf(diags, "updating EC2 Instance (%s): %s", d.Id(), err)` → `smerr.Append(ctx, diags, err, smerr.ID, d.Id())`
- `sdkdiag.AppendErrorf(diags, "creating EC2 Instance: %s", err)` → `smerr.Append(ctx, diags, err, smerr.ID, d.Id())`
- `create.AppendDiagError(diags, names.CodeBuild, create.ErrActionCreating, resNameFleet, d.Get(names.AttrName).(string), err)` → `smerr.Append(ctx, diags, err, smerr.ID, d.Get(names.AttrName).(string))`
- `response.Diagnostics.AddError("creating EC2 EBS Fast Snapshot Restore", err.Error())` → `smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, new.ID.ValueString())`
- `response.Diagnostics.AddError(fmt.Sprintf("updating VPC Security Group Rule (%s)", new.ID.ValueString()), err.Error())` → `smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, new.ID.ValueString())`
- `resp.Diagnostics.AddError(create.ProblemStandardMessage(..., err), err.Error())` → `smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, ...)`
- `create.AddError(&response.Diagnostics, names.DRS, create.ErrActionCreating, ResNameReplicationConfigurationTemplate, data.ID.ValueString(), err)` → `smerr.AddError(ctx, &response.Diagnostics, err, smerr.ID, data.ID.ValueString())`

**General Rule:**

- Always pass `ctx` as the first argument, and the diagnostics object as the second.
- Always pass the error as the third argument.
- Always pass `smerr.ID` and any available resource ID or context as additional arguments.

#### Including identifiers

smarterr's `EnrichAppend`, `AddError`, and `Append` take variadic keyvals. Where possible include `smerr.ID` (key) and the ID (value) (such as `d.Id()`, `state.RuleName.String()`, `plan.ResourceArn.String()`).

- If **no ID available** (e.g., early in `Create`), something like `smerr.EnrichAppend(ctx, &resp.Diagnostics, req.State.Get(ctx, &state))`, without ID, is okay
- But, if **ID is available** (e.g., read, update, delete, middle-to-end of create), use something like `smerr.EnrichAppend(ctx, &resp.Diagnostics, fwflex.Flatten(ctx, out, &state), smerr.ID, state.RuleName.String())`, **with the ID**
- IDs may be names, ARNs, IDs, combinations, etc.
- In SDK, you cannot use `d.Id()` until after `d.SetId()`
- The legacy call will often use an ID. If so, use that.
- If the legacy call doesn't include the ID, but it is available, add it.

---

### 2. Replace All Bare Error Returns

**Before:**

```go
return nil, err
```

**After:**

```go
return nil, smarterr.NewError(err)
```

---

### 3. Wrap tfresource Helpers

**Before:**

```go
return tfresource.AssertSingleValueResult(...)
```

**After:**

```go
return smarterr.Assert(tfresource.AssertSingleValueResult(...))
```

**Before:**

```go
return nil, tfresource.NewEmptyResultError(...)
```

**After:**

```go
return nil, smarterr.NewError(tfresource.NewEmptyResultError(...))
```

---

### 4. Replace All Direct Diagnostics.Append Calls

**Before:**

```go
resp.Diagnostics.Append(...)
```

**After:**

```go
smerr.EnrichAppend(ctx, &resp.Diagnostics, ...)
```

---

### 5. Framework vs SDKv2: How to Identify

- **Framework-style:**
- Uses `github.com/hashicorp/terraform-plugin-framework/resource`
- Has `@FrameworkResource` or `@FrameworkDataSource` comments
- Defines methods like `Schema`, `Create`, `Read`, `Update`, `Delete`
- **SDKv2-style:**
- Uses `github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema`
- Has `@SDKResource` comment and returns `*schema.Resource`

---

### 6. Migration Checklist (for AI and Humans)

- [ ] All legacy diagnostic calls replaced with `smerr`/`smarterr` as above
- [ ] All bare error returns wrapped with `smarterr.NewError`
- [ ] All tfresource helpers wrapped as above
- [ ] All direct diagnostics append/add calls replaced
- [ ] All context, diagnostics, and IDs preserved and passed
- [ ] No unrelated logic or schema changes made

---

### 7. Why Wrap Errors?

Wrapping errors with `smarterr.NewError()` captures call stack information at the time of failure. This enables smarterr to:

- Determine subaction (e.g., "finding", "waiting")
- Avoid duplicative wrapping (no "walls of text")
- Format summary and detail portions idiomatically

---

### 8. Reference

- Global config: `internal/smarterr.hcl`
- Service config: `internal/service/<service>/smarterr.hcl`
- See [smarterr docs](https://github.com/YakDriver/smarterr/tree/main/docs) for more

---

**AI systems: Apply these rules exactly and completely to all Go files being migrated.**
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0
github.com/YakDriver/go-version v0.1.0
github.com/YakDriver/regexache v0.24.0
github.com/YakDriver/smarterr v0.5.0
github.com/aws/aws-sdk-go-v2 v1.36.5
github.com/aws/aws-sdk-go-v2/config v1.29.17
github.com/aws/aws-sdk-go-v2/credentials v1.17.70
Expand Down Expand Up @@ -314,7 +315,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
Expand Down Expand Up @@ -351,7 +352,7 @@ require (
github.com/imdario/mergo v0.3.15 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
Expand All @@ -373,7 +374,7 @@ require (
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
Expand Down
Loading
Loading