Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions scripts/remove_custom_property/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Remove a Custom Property Across OpsLevel Infra Types

`remove_custom_property.py` deletes a custom property (default: `name`) from all
AWS-backed infrastructure component types via the OpsLevel GraphQL API. Each type
holds its own copy of the property, so the script finds all infrastructure component types then deletes the custom property from each type.
(`propertyDefinitionDelete` — there's no bulk mutation). Dry-run by default.

## Setup
```
pip install requests
export OPSLEVEL_API_TOKEN=xxxxxxxx
```

## Run

```
python3 remove_custom_property.py # 1. dry run: confirms matches, deletes nothing
# edit PREFIX="aws_dynamodb", APPLY=True # 2. delete one type first to confirm it works
# edit PREFIX="aws_", keep APPLY=True # 3. run the rest
```
Output per type: would delete <id> (dry run), deleted, FAILED [...], or no 'name' (skipped). No bulk rollback, so the log is your record.
## Config (top of file)

- `PROP` — property to remove (alias or name). Default `name`.
- `PREFIX` — alias prefix that marks a type as "infra". Default `aws_` (the API has no infra filter, so we match by prefix).
- `APPLY` — `False` = dry run, `True` = delete.

## Notes

- Deletes are **irreversible** and drop stored values — dry-run, test one, then run all.
- `name` ("The name of the resource") may be a built-in field; a `FAILED` line means the API protects it. To hide instead of delete, use `propertyDefinitionUpdate` with `propertyDisplayStatus: hidden`.
59 changes: 59 additions & 0 deletions scripts/remove_custom_property/remove_custom_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os, requests # os: read the API token from your environment; requests: HTTP calls

# ---- Configuration: the only lines you normally touch -----------------------
TOKEN = os.environ["OPSLEVEL_API_TOKEN"] # your OpsLevel API token (set it in the shell, not in the file)
URL = "https://app.opslevel.com/graphql" # OpsLevel's single GraphQL endpoint
PROP = "name" # the property to remove (matched by alias OR display name)
PREFIX = "aws_" # which component types count as "infra": those whose alias starts with this
APPLY = False # safety switch: False = dry run (prints only), True = actually deletes

def gql(q, v=None):
r = requests.post(URL, headers={"Authorization": f"Bearer {TOKEN}"}, json={"query": q, "variables": v or {}})
r.raise_for_status(); d = r.json() # raise if HTTP failed (401 bad token, 5xx, etc.)
if d.get("errors"): raise SystemExit(d["errors"]) # GraphQL can return 200 OK but still carry errors
return d["data"] # the useful payload lives under "data"

def infra_types():
"""Return every component type whose alias starts with PREFIX (your 21 infra types)."""
# componentTypes returns ALL types (Services, Teams, infra...) so we filter ourselves.
# It's paginated (max 100 per page), so we loop, passing the previous page's cursor.

q = "query($a:String){account{componentTypes(first:100,after:$a){nodes{id name aliases} pageInfo{hasNextPage endCursor}}}}"

out, after = [], None # accumulated matches, and the paging cursor (None = first page)
while True:
c = gql(q, {"a": after})["account"]["componentTypes"]

# keep only nodes where any alias begins with PREFIX
out += [n for n in c["nodes"] if any((a or "").startswith(PREFIX) for a in (n["aliases"] or []))]

if not c["pageInfo"]["hasNextPage"]: return out # no more pages -> done

after = c["pageInfo"]["endCursor"] # otherwise fetch the next page

def delete_name():
"""For each infra type: find the PROP definition and delete it (or preview it)."""
# PQ: given one component type, list its property definitions (id + aliases + name).
PQ = "query($i:IdentifierInput!){account{componentType(input:$i){properties(first:100){nodes{id aliases name}}}}}"

# DEL: delete one property definition by its id; returns any per-item errors.
DEL = "mutation($r:IdentifierInput!){propertyDefinitionDelete(resource:$r){errors{message}}}"
types = infra_types()
print(f"{len(types)} infra type(s) matched prefix '{PREFIX}' (expecting 21)")
for t in types:
alias = (t["aliases"] or [t["name"]])[0] # a readable label for logging

# Each component type has its OWN copy of the property, so we look it up per type by id.
props = gql(PQ, {"i": {"id": t["id"]}})["account"]["componentType"]["properties"]["nodes"]

# find the property whose alias matches PROP exactly, or whose name matches case-insensitively
hit = next((p for p in props if PROP in (p["aliases"] or []) or p["name"].lower() == PROP.lower()), None)

if not hit: print(f"{alias}: no '{PROP}'"); continue # this type doesn't have the property
if not APPLY: print(f"{alias}: would delete {hit['id']}"); continue # dry run: show what *would* happen, change nothing

# live delete: send the mutation, then read back any errors for THIS type
errs = gql(DEL, {"r": {"id": hit["id"]}})["propertyDefinitionDelete"]["errors"]
print(f"{alias}: {'FAILED ' + str(errs) if errs else 'deleted'}")

delete_name() # run it