Metadata-Version: 2.5
Name: strayform
Version: 0.1.0
Summary: Find Google Cloud resources created outside Terraform (ClickOps) and generate import blocks to adopt them.
Project-URL: Homepage, https://github.com/MarckMorris/strayform
Project-URL: Issues, https://github.com/MarckMorris/strayform/issues
Author: Marck Morris
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: clickops,devops,drift,gcp,google-cloud,iac,sre,terraform
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Requires-Dist: google-cloud-asset>=3.26
Requires-Dist: google-cloud-resource-manager>=1.12
Requires-Dist: google-cloud-storage>=2.16
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# strayform

**Find Google Cloud resources that live outside Terraform — and adopt them in one step.**

Someone created a VM "just for debugging". A bucket was spun up from the console for a one-off export. A node pool was added by hand during an incident. Months later nobody knows who owns them, they're not in code review, not in your cost model, and not covered by your policies.

`strayform` compares what actually runs in GCP (via [Cloud Asset Inventory](https://cloud.google.com/asset-inventory/docs/overview)) with what your Terraform state says you manage, and reports:

- **Unmanaged** — resources created outside Terraform (ClickOps).
- **Ghosts** — resources in your state that no longer exist in GCP (deleted by hand).
- **IaC coverage** — the % of your supported infrastructure that is under Terraform.

Then it writes Terraform 1.5+ `import {}` blocks so you can bring the strays under management with `terraform plan -generate-config-out`.

```text
$ strayform scan -s gs://acme-tfstate/prod -p acme-prod
IaC coverage 66.7% — managed 6, unmanaged 3, ghosts 1, ignored 3
                  Unmanaged resources (ClickOps)
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Terraform type             ┃ ID                                                           ┃ Created    ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ google_compute_instance    │ projects/acme-prod/zones/us-central1-a/instances/debug-vm    │ 2026-09-01 │
│ google_container_node_pool │ projects/acme-prod/locations/us-central1/clusters/core/...   │            │
│ google_storage_bucket      │ acme-prod-tmp-export                                         │            │
└────────────────────────────┴──────────────────────────────────────────────────────────────┴────────────┘
```

## Why another drift tool?

`terraform plan` only sees resources that are already in state — it can't tell you what was created *around* it. [driftctl](https://github.com/snyk/driftctl), the best-known tool for this, has been in maintenance mode since 2023 and its GCP coverage was limited. `strayform` is GCP-first:

- Uses **Cloud Asset Inventory**, so one API call covers a project, a folder or a whole organization.
- **Knows GCP's noise**: GKE nodes and boot disks, PVC disks, Kubernetes load balancer rules, the default VPC, Cloud Functions/Cloud Build buckets and other resources that Google creates for you are ignored by default (see [`rules.py`](src/strayform/rules.py)).
- Normalizes project **numbers vs IDs** and the different ID formats Terraform uses (self links, bare names, `project/location/cluster/pool`).
- Generates **ready-to-use import blocks**, not just a report.
- **CI-friendly**: JSON / Markdown output and `--fail-on-drift`.

## Install

```bash
pip install strayform          # or: pipx install strayform
```

Requires Python 3.10+ and [Application Default Credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc):

```bash
gcloud auth application-default login
```

The identity needs `roles/cloudasset.viewer` on the scope, `roles/browser` (to resolve project numbers) and read access to the state bucket. Enable the API once: `gcloud services enable cloudasset.googleapis.com`.

## Usage

```bash
# One project, local state
strayform scan -s terraform.tfstate -p my-project

# Several states (a directory or a GCS prefix) against a whole folder
strayform scan -s gs://my-tfstate/ -p folders/123456789012

# Generate import blocks, then let Terraform write the HCL
strayform scan -s gs://my-tfstate/prod -p my-project --generate-imports imports.tf
terraform plan -generate-config-out=generated.tf

# CI: markdown for a PR comment, non-zero exit when drift exists
strayform scan -s gs://my-tfstate/prod -p my-project -o markdown --fail-on-drift > drift.md

# Supported resource types
strayform types
```

Exit codes: `0` no drift (or drift without `--fail-on-drift`), `1` error, `2` drift found.

### Remote state

`strayform` reads state files directly (`*.tfstate`, format v4) from local paths or `gs://` — the default layout of the Terraform `gcs` backend (`<prefix>/<workspace>.tfstate`). For other backends, export first: `terraform state pull > current.tfstate`.

### Ignore rules

Add a `.strayform.yaml` next to your code:

```yaml
ignore:
  - type: storage.googleapis.com/Bucket
    name: "*-scratch-*"
    reason: personal scratch buckets
  - type: "compute.googleapis.com/*"
    label: managed-by=config-connector
    reason: owned by Config Connector
```

`name` is a glob matched against the resource's short name or full path; `label` is `key` or `key=value`. Use `--no-default-ignores` to see everything, including the Google-managed resources.

## Supported resources

Compute (instances, disks, networks, subnets, firewalls, addresses, routers, global and regional instance templates, backend services and forwarding rules), Cloud Storage, BigQuery datasets and tables, Cloud SQL, Memorystore Redis, Pub/Sub, GKE clusters and node pools, Cloud Run (v1 and v2), Artifact Registry, Secret Manager and Cloud KMS key rings. Run `strayform types` for the full mapping.

A type is only added when its Cloud Asset Inventory name and its Terraform import ID map to each other deterministically, so the tool never guesses. Contributions for more types are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).

## Limitations

- Cloud Asset Inventory is eventually consistent: resources created or deleted in the last few minutes may not be reflected yet.
- Only resources whose type is supported are compared; coverage is computed over those types.
- Ghosts are only reported for projects inside the scanned scope.

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check .
```

## License

Apache 2.0
