|
| 1 | +# Provider `ReadDataSource` Crash — `os.Exit(1)` from Atmos Library |
| 2 | + |
| 3 | +**Affected Versions:** `cloudposse/utils` provider v2.0.0 (Atmos v1.207.0 embedded) |
| 4 | + |
| 5 | +**Severity:** Critical — provider process exits with code 1 during `ReadDataSource` |
| 6 | +calls, producing "Plugin did not respond" errors in Terraform |
| 7 | + |
| 8 | +## Symptoms |
| 9 | + |
| 10 | +Components with multiple `data "utils_component_config"` data sources crash the provider during |
| 11 | +`terraform plan`: |
| 12 | + |
| 13 | +```text |
| 14 | +Error: Plugin did not respond |
| 15 | +
|
| 16 | + with module.iam_roles.module.account_map.data.utils_component_config.config[0], |
| 17 | + on .terraform/modules/iam_roles.account_map/modules/remote-state/main.tf line 1, |
| 18 | + in data "utils_component_config" "config": |
| 19 | + 1: data "utils_component_config" "config" { |
| 20 | +``` |
| 21 | + |
| 22 | +`TF_LOG=TRACE` reveals the provider exits with **exit status 1** (not a panic or signal): |
| 23 | + |
| 24 | +```text |
| 25 | +provider: plugin process exited: path=...terraform-provider-utils pid=3934 error="exit status 1" |
| 26 | +``` |
| 27 | + |
| 28 | +## Root Causes |
| 29 | + |
| 30 | +Investigation revealed **two independent issues** that can each trigger `os.Exit(1)` inside the |
| 31 | +Atmos library, killing the provider's gRPC plugin process. Both are addressed in this fix. |
| 32 | + |
| 33 | +### Issue 1: `os.Exit(1)` on errors in the Atmos library |
| 34 | + |
| 35 | +The Atmos library uses `CheckErrorPrintAndExit()` in many code paths within `internal/exec`. |
| 36 | +This function is designed for CLI usage — it prints an error and exits the process. Inside a |
| 37 | +Terraform provider (gRPC plugin), calling `os.Exit(1)` terminates the plugin without returning |
| 38 | +a diagnostic error to Terraform. |
| 39 | + |
| 40 | +Any error that reaches `CheckErrorPrintAndExit` silently crashes the provider. Code paths that |
| 41 | +use it include: |
| 42 | + |
| 43 | +- `utils.go:664-673` — duplicate component config detection |
| 44 | +- `utils.go:753,765` — template processing errors |
| 45 | +- `yaml_func_store.go:29,89,99,107` — `!store` YAML tag errors |
| 46 | +- `yaml_func_store_get.go:52,90,101,116` — `!store.get` YAML tag errors |
| 47 | +- `describe_stacks.go:489,743,982` — describe stacks errors |
| 48 | + |
| 49 | +**Any** error reaching these paths will crash the provider via `os.Exit(1)`. The provider cannot |
| 50 | +intercept `os.Exit` — the Atmos library kills the process before the provider can return a |
| 51 | +diagnostic to Terraform. |
| 52 | + |
| 53 | +### Issue 2: Thread-unsafe global state (LATENT) |
| 54 | + |
| 55 | +The Atmos library was designed as a single-threaded CLI tool. It has **package-level mutable |
| 56 | +state** that is explicitly documented as not thread-safe: |
| 57 | + |
| 58 | +```go |
| 59 | +// pkg/config/load.go:51-54 |
| 60 | +// NOTE: This package-level state assumes sequential (non-concurrent) calls to LoadConfig. |
| 61 | +// LoadConfig is NOT safe for concurrent use. |
| 62 | +var mergedConfigFiles []string |
| 63 | +``` |
| 64 | + |
| 65 | +Additional thread-unsafe global state includes: |
| 66 | + |
| 67 | +- `errors/error_funcs.go:31` — `var atmosConfig *schema.AtmosConfiguration` |
| 68 | +- `errors/error_funcs.go:28` — `var render *markdown.Renderer` |
| 69 | +- `errors/error_funcs.go:34` — `var verboseFlag` |
| 70 | + |
| 71 | +Terraform invokes `ReadDataSource` concurrently (one goroutine per data source instance). Each |
| 72 | +call enters `ProcessComponentInStack` → `InitCliConfig` → `LoadConfig`, which resets and writes |
| 73 | +to the shared `mergedConfigFiles` slice. Concurrent goroutines can corrupt the slice through |
| 74 | +interleaved reads/writes, causing downstream errors that hit `CheckErrorPrintAndExit` → |
| 75 | +`os.Exit(1)`. |
| 76 | + |
| 77 | +This is a **latent** issue — it is a real data race that could cause unpredictable failures |
| 78 | +under concurrent load. |
| 79 | + |
| 80 | +### Debug log timeline (provider v2.0.0, pid=3934) |
| 81 | + |
| 82 | +| Timestamp | Event | |
| 83 | +|----------------|--------------------------------------------------------------| |
| 84 | +| `19:24:15.825` | Provider starts, configures mTLS | |
| 85 | +| `19:24:15.941` | GetProviderSchema — success | |
| 86 | +| `19:24:15.945` | Configure — success | |
| 87 | +| `19:24:16.117` | ValidateDataSourceConfig — success | |
| 88 | +| `19:24:16.122` | **ReadDataSource #1** — "Calling downstream" (never returns) | |
| 89 | +| `19:24:16.152` | **ReadDataSource #2** — "Calling downstream" (never returns) | |
| 90 | +| `19:24:16.276` | **ReadDataSource #3** — "Calling downstream" (never returns) | |
| 91 | +| `19:24:16.817` | **Plugin process exited: exit status 1** | |
| 92 | +| `19:24:16.817` | gRPC: "connection reset by peer" | |
| 93 | + |
| 94 | +## Fix |
| 95 | + |
| 96 | +### Provider-side fix |
| 97 | + |
| 98 | +Add a package-level `sync.Mutex` in the provider to serialize all calls into the Atmos library. |
| 99 | +This addresses **Issue 2** by preventing concurrent goroutines from accessing the thread-unsafe |
| 100 | +global state simultaneously. It does not prevent `os.Exit(1)` from stack config errors (Issue 1), |
| 101 | +but it eliminates the data race as a potential trigger. |
| 102 | + |
| 103 | +**New file: `internal/provider/atmos_lock.go`** |
| 104 | + |
| 105 | +```go |
| 106 | +package provider |
| 107 | + |
| 108 | +import "sync" |
| 109 | + |
| 110 | +// atmosMu serializes all calls into the Atmos library. |
| 111 | +// The Atmos library uses package-level mutable state (e.g., mergedConfigFiles in |
| 112 | +// pkg/config/load.go) that is explicitly documented as not safe for concurrent use. |
| 113 | +// Terraform invokes ReadDataSource concurrently for independent data sources, so |
| 114 | +// without this mutex, concurrent calls corrupt shared state and trigger os.Exit(1) |
| 115 | +// via CheckErrorPrintAndExit, killing the gRPC plugin process. |
| 116 | +var atmosMu sync.Mutex |
| 117 | +``` |
| 118 | + |
| 119 | +**Modified data source files:** |
| 120 | + |
| 121 | +Each `ReadContext` function wraps its Atmos library calls with `atmosMu.Lock()` / |
| 122 | +`atmosMu.Unlock()`: |
| 123 | + |
| 124 | +- `data_source_component_config.go` — wraps `ProcessComponentInStack` / `ProcessComponentFromContext` |
| 125 | +- `data_source_describe_stacks.go` — wraps `InitCliConfig` + `ExecuteDescribeStacks` |
| 126 | +- `data_source_stack_config_yaml.go` — wraps `InitCliConfig` + `ProcessYAMLConfigFiles` |
| 127 | +- `data_source_spacelift_stack_config.go` — wraps `CreateSpaceliftStacks` |
| 128 | +- `data_source_aws_eks_update_kubeconfig.go` — wraps `ExecuteAwsEksUpdateKubeconfig` |
| 129 | + |
| 130 | +### Long-term fix (Atmos library) |
| 131 | + |
| 132 | +The Atmos library should be refactored to: |
| 133 | + |
| 134 | +1. Replace `CheckErrorPrintAndExit` / `os.Exit` calls in library code paths with proper error |
| 135 | + returns, so embedded consumers (like this provider) can handle errors gracefully — this is |
| 136 | + the most critical fix, as it would convert silent crashes into visible Terraform diagnostics |
| 137 | +2. Eliminate package-level mutable state in `pkg/config` and `errors` |
| 138 | +3. Pass configuration through context or options structs instead of global variables |
| 139 | + |
| 140 | +## References |
| 141 | + |
| 142 | +- Atmos `LoadConfig` thread-safety comment: `pkg/config/load.go:51-54` |
| 143 | +- `CheckErrorPrintAndExit` implementation: `errors/error_funcs.go:324-366` |
0 commit comments