Skip to content

Commit e45f335

Browse files
authored
Merge pull request #54 from Jackass4life/copilot/fix-docker-compose-redis-error
Fix docker-compose usage and resolve Nautobot 3.x brief tag objects in CI
2 parents 6666d22 + ce26a8d commit e45f335

5 files changed

Lines changed: 105 additions & 10 deletions

File tree

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,16 @@ python app.py
7070

7171
## Docker
7272

73+
> **Important:** Always use `docker compose up`**not** `docker compose build && docker compose start`.
74+
> The `start` sub-command only restarts previously created containers and will
75+
> fail with *"service … has no container to start"* on a fresh checkout.
76+
> `docker compose up` handles building, creating, and starting in one step.
77+
7378
```bash
74-
# Build and run with Docker Compose
75-
cp .env.example .env # fill in your values
79+
# 1. Configure environment variables
80+
cp .env.example .env # fill in NAUTOBOT_URL and NAUTOBOT_TOKEN
81+
82+
# 2. Build images and start containers
7683
docker compose up --build -d
7784
# → Open http://localhost:5000
7885

@@ -83,11 +90,6 @@ docker compose logs -f
8390
docker compose down
8491
```
8592

86-
> **Note:** Always use `docker compose up` (not `docker compose start`) to create
87-
> and start the containers. The `start` sub-command only restarts previously
88-
> created containers and will fail with *"has no container to start"* on a fresh
89-
> checkout.
90-
9193
## Demo (Mock Nautobot)
9294

9395
No Nautobot instance? Spin up a fully self-contained demo using the mock

app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def get_locations() -> list:
223223
tenant_map = _build_id_name_map("tenancy/tenants/")
224224
status_map = _build_id_name_map("extras/statuses/")
225225
lt_map = _build_id_name_map("dcim/location-types/")
226+
tag_map = _build_id_name_map("extras/tags/")
226227
tenant_group_map = _build_tenant_group_map()
227228

228229
# Build a location id → name map from the raw data for parent resolution.
@@ -286,10 +287,19 @@ def get_locations() -> list:
286287
tenant_group_name = tenant_group_map.get(tenant_id, "")
287288

288289
# Tags – each tag is a nested object with at least a name/display key.
290+
# In Nautobot 3.x brief tag objects may only contain id+url, so fall
291+
# back to the pre-built tag_map.
289292
raw_tags = loc.get("tags") or []
290293
tag_names = []
291294
for t in raw_tags:
292-
tag_name = _nested_str(t, "name", "display") if isinstance(t, dict) else ""
295+
if isinstance(t, dict):
296+
tag_id = t.get("id", "")
297+
tag_name = (
298+
_nested_str(t, "name", "display")
299+
or tag_map.get(tag_id, "")
300+
)
301+
else:
302+
tag_name = ""
293303
if tag_name:
294304
tag_names.append(tag_name)
295305

development/seed_nautobot.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,39 @@ def _post(endpoint: str, data: dict) -> dict:
9292
return resp.json()
9393

9494

95+
def _patch(url: str, data: dict) -> dict:
96+
"""PATCH helper with error handling."""
97+
resp = session.patch(url, json=data, timeout=30)
98+
if not resp.ok:
99+
try:
100+
body = resp.json()
101+
except Exception:
102+
body = resp.text[:500]
103+
print(f" ERROR {resp.status_code} PATCH {url}: {body}", file=sys.stderr)
104+
resp.raise_for_status()
105+
return resp.json()
106+
107+
95108
def get_or_create(endpoint: str, data: dict, lookup: dict | None = None) -> dict:
96109
"""
97110
Return an existing object or create a new one.
111+
If the object exists, update it with the new data.
98112
99113
*lookup* defaults to ``{"name": data["name"]}`` if not given.
100114
"""
101115
lookup = lookup or {"name": data.get("name", data.get("model", ""))}
102116
results = _get(endpoint, params=lookup).get("results", [])
103117
if results:
104-
print(f" ↳ found {endpoint} {lookup}")
105-
return results[0]
118+
existing = results[0]
119+
obj_url = existing.get("url")
120+
if obj_url:
121+
# Update the existing object with new data
122+
obj = _patch(obj_url, data)
123+
print(f" ↳ updated {endpoint} {lookup}")
124+
return obj
125+
else:
126+
print(f" ↳ found {endpoint} {lookup}")
127+
return existing
106128

107129
obj = _post(endpoint, data)
108130
print(f" ↳ created {endpoint} {lookup}")

docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# docker-compose.yml - Nautobot Maps (production / quick-start)
2+
#
3+
# Start: docker compose up --build -d
4+
# Logs: docker compose logs -f
5+
# Stop: docker compose down
6+
#
7+
# NOTE: Do NOT use "docker compose build && docker compose start".
8+
# "start" only restarts previously created containers and will fail with
9+
# "has no container to start" on a fresh checkout. Always use
10+
# "docker compose up" which both creates and starts containers.
11+
112
services:
213
redis:
314
image: redis:7-alpine

tests/test_app.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,56 @@ def test_tags_empty_when_none(self, client):
212212
loc = resp.get_json()["locations"][1]
213213
assert loc["tags"] == []
214214

215+
def test_tags_fallback_with_brief_nested_object(self, client):
216+
"""When tags are brief (id+url only), the fallback map resolves names."""
217+
brief_locations = {
218+
"count": 1,
219+
"next": None,
220+
"results": [
221+
{
222+
"id": "loc-tags",
223+
"name": "Tagged Location",
224+
"slug": "tagged-loc",
225+
"status": {"label": "Active"},
226+
"location_type": {"name": "Data Center"},
227+
"parent": None,
228+
"latitude": "55.0",
229+
"longitude": "12.0",
230+
"description": "",
231+
"physical_address": "",
232+
"tenant": None,
233+
"asn": None,
234+
"time_zone": "",
235+
"facility": "",
236+
"tags": [
237+
{"id": "tag-1", "url": "http://nautobot/api/extras/tags/tag-1/"},
238+
{"id": "tag-2", "url": "http://nautobot/api/extras/tags/tag-2/"},
239+
],
240+
"url": "",
241+
},
242+
],
243+
}
244+
tags_page = {
245+
"count": 2,
246+
"next": None,
247+
"results": [
248+
{"id": "tag-1", "name": "critical"},
249+
{"id": "tag-2", "name": "production"},
250+
],
251+
}
252+
253+
def mock_get(endpoint, params=None):
254+
if "extras/tags" in endpoint:
255+
return tags_page
256+
if "dcim/locations" in endpoint:
257+
return brief_locations
258+
return {"count": 0, "next": None, "results": []}
259+
260+
with patch.object(flask_app, "nautobot_get", side_effect=mock_get):
261+
resp = client.get("/api/locations")
262+
loc = resp.get_json()["locations"][0]
263+
assert loc["tags"] == ["critical", "production"]
264+
215265
def test_location_type_fallback_with_brief_nested_object(self, client):
216266
"""When location_type is brief (id+url only), the fallback map resolves the name."""
217267
brief_locations = {

0 commit comments

Comments
 (0)