Skip to content

Commit 9228bee

Browse files
authored
Merge pull request #168 from bcgov/feature/oc-backup-db-to-unc
scripts: UNC-share pg_dump backup + artifactory rotation fix
2 parents 26f8edd + 264c0b2 commit 9228bee

5 files changed

Lines changed: 475 additions & 9 deletions

File tree

.github/workflows/deploy-instance.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -520,12 +520,14 @@ jobs:
520520
INSTANCE: ${{ needs.metadata.outputs.instance-name }}
521521
run: |
522522
set -euo pipefail
523-
# For prod, rotate the SHA-tagged rollback handles: keep the 10 most
524-
# recent `<instance>-*` named tags per image, delete the rest. Then
525-
# orphan cleanup reclaims underlying layers.
523+
# For prod, rotate the SHA-tagged rollback handles: keep the 3 most
524+
# recent `<instance>-<12hex>` named tags per image, delete the rest.
525+
# The 12-`?` glob matches only the commit-SHA-suffixed tags, so
526+
# `<instance>` and `<instance>-test` are never rotated. Orphan cleanup
527+
# then reclaims underlying layers.
526528
# For test/dev, just orphan cleanup — no SHA tags accumulate.
527529
if [[ "${ENVIRONMENT}" == "prod" ]]; then
528-
bash ./scripts/artifactory-cleanup.sh --delete --keep 10 --match "${INSTANCE}-*"
530+
bash ./scripts/artifactory-cleanup.sh --delete --keep 3 --match "${INSTANCE}-????????????"
529531
else
530532
bash ./scripts/artifactory-cleanup.sh --delete
531533
fi
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Backing up the production database to a Windows network share
2+
3+
This page describes `scripts/oc-backup-db-to-unc.sh`, a variant of the standard
4+
backup script that streams a `pg_dump` of an instance's PostgreSQL database
5+
straight to a Windows UNC share — without ever creating a file on the WSL host.
6+
7+
Use it when the backup must not be persisted on the local machine (for example,
8+
when capturing production into a managed file-share location such as
9+
`\\widget\SDPRDocuments`).
10+
11+
## When to use this vs. `oc-backup-db.sh`
12+
13+
| Question | Use `oc-backup-db.sh` | Use `oc-backup-db-to-unc.sh` |
14+
|----------|-----------------------|-------------------------------|
15+
| Output destination | `./backups/<file>.pgc` on the WSL host | `\\server\share\<file>.pgc` on Windows |
16+
| Local file allowed? | Yes | No — nothing is written locally |
17+
| Platform | Any host with `oc` | WSL only (uses `powershell.exe`) |
18+
| Restore workflow | Pass local file to `oc-restore-db.sh --from` | File must first be copied somewhere local before restore |
19+
20+
## Prerequisites
21+
22+
- WSL with Windows interop enabled (`powershell.exe` reachable at
23+
`/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`).
24+
- The UNC share is reachable from the Windows side and the current Windows
25+
user has write access to it.
26+
- `.oc-deploy/token` exists — created by `scripts/oc-setup-sa.sh` against the
27+
target namespace (e.g. `fd34fb-prod`).
28+
- The service account has `pods/exec` permission (granted during setup).
29+
30+
## Usage
31+
32+
```bash
33+
# Production (instance bcgov-di in namespace fd34fb-prod)
34+
./scripts/oc-backup-db-to-unc.sh --instance bcgov-di --dest '\\widget\SDPRDocuments'
35+
36+
# Defaulting the instance to the current git branch
37+
./scripts/oc-backup-db-to-unc.sh --dest '\\widget\SDPRDocuments'
38+
```
39+
40+
Output file: `<dest>\<instance>-<YYYYMMDD-HHMMSS>.pgc`, e.g.
41+
`\\widget\SDPRDocuments\bcgov-di-20260515-082000.pgc`.
42+
43+
The dump uses `pg_dump -Fc --clean --if-exists` (custom format with drop/recreate
44+
statements). Restore with `pg_restore` or the project's `oc-restore-db.sh`.
45+
46+
## How "no local file" is achieved
47+
48+
The standard `oc-backup-db.sh` writes the dump to `/tmp/...` inside the pod,
49+
then `oc cp`s it to the local filesystem. This script replaces the second
50+
step with a direct stream:
51+
52+
1. `oc exec <pod> -c database -- pg_dump -Fc ...` writes the dump to stdout
53+
(no TTY, so the binary custom-format stream is preserved end-to-end).
54+
2. Bash pipes that stdout into `powershell.exe -NoProfile -Command '...'`.
55+
3. The PowerShell side reads raw bytes via `[Console]::OpenStandardInput()`
56+
and writes them to `[System.IO.File]::Create('<UNC path>')`. Using the
57+
stream APIs (not `Set-Content`) keeps the byte sequence intact — no
58+
CRLF translation, no implicit decoding.
59+
4. PowerShell then queries `Get-Item <dest>` for `.Length` and reports the
60+
byte count back. The dump itself is never opened for reading.
61+
62+
No file is created on the WSL filesystem or on a local Windows drive — the
63+
bytes traverse the pipe and land on the network share.
64+
65+
## Before-stream safety check
66+
67+
Before invoking `pg_dump`, the script writes a 2-byte probe file to the
68+
destination via PowerShell and deletes it again. If the probe fails (share
69+
unreachable, no write permission, path doesn't exist), the script aborts
70+
before any database work begins. This avoids running a multi-minute dump only
71+
to discover the destination is unwritable.
72+
73+
## Restoring a dump that lives on a network share
74+
75+
`scripts/oc-restore-db.sh` expects a local file path, so to restore you must
76+
first copy the `.pgc` file to a location the restore script can read.
77+
78+
```bash
79+
# From WSL, copy back via powershell.exe (or any Windows file-copy tool)
80+
powershell.exe -NoProfile -Command "Copy-Item '\\widget\SDPRDocuments\bcgov-di-20260515-082000.pgc' 'C:\temp\restore.pgc'"
81+
./scripts/oc-restore-db.sh --instance bcgov-di-test --from /mnt/c/temp/restore.pgc
82+
```
83+
84+
The restore script uses `pg_restore` and respects the `--clean --if-exists`
85+
flags baked into the dump, so existing data in the target instance is
86+
dropped and replaced.
87+
88+
## What is *not* backed up
89+
90+
The script captures only the PostgreSQL database. Azure Blob Storage content
91+
(uploaded documents, attachments, etc.) is **not** included; it must be
92+
backed up separately. This matches the behavior of `oc-backup-db.sh`.
93+
94+
## Troubleshooting
95+
96+
| Symptom | Likely cause |
97+
|---------|--------------|
98+
| `powershell.exe not found at /mnt/c/Windows/...` | Not running under WSL, or Windows interop is disabled. |
99+
| `Destination not writable` | UNC share is unreachable, or the Windows user lacks write permission. Try `powershell.exe -Command "Test-Path '<dest>'"` to verify. |
100+
| `Backup stream failed` | Either `oc exec` lost its connection, or the PowerShell side errored. A partial file may exist at the destination — delete it before retrying. |
101+
| `Token may have expired` | Re-run `./scripts/oc-setup-sa.sh --namespace <namespace>` to issue a fresh token. |
102+
| Backup file is much smaller than expected | The database may genuinely be small, but also check stderr from `pg_dump` (printed inline) for table-level errors. |

scripts/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,33 @@ Notes:
206206

207207
---
208208

209+
### oc-backup-db-to-unc.sh — Database Backup to Windows UNC share
210+
211+
Streams a `pg_dump` of an instance's PostgreSQL database directly to a Windows
212+
UNC share (e.g. `\\widget\SDPRDocuments`) without writing any file on the WSL
213+
host. Use this when the backup must stay off the local machine.
214+
215+
```bash
216+
# Stream prod backup to a UNC share
217+
./scripts/oc-backup-db-to-unc.sh --instance bcgov-di --dest '\\widget\SDPRDocuments'
218+
```
219+
220+
| Option | Short | Required | Description |
221+
|--------|-------|----------|-------------|
222+
| `--dest` | `-d` | Yes | UNC directory to write into (e.g. `\\server\share`) |
223+
| `--instance` | `-i` | No | Instance to back up (default: from git branch) |
224+
| `--help` | `-h` | No | Show help |
225+
226+
Output: `<UNC-dest>\<instance>-<timestamp>.pgc` (pg_dump custom format, `-Fc`)
227+
228+
Notes:
229+
- WSL-only — requires Windows interop with `powershell.exe` reachable
230+
- Verifies the destination is writable before starting the dump
231+
- Pipes `oc exec ... pg_dump` stdout through `powershell.exe` to a `[System.IO.File]::Create` stream — no intermediate file on either Linux or Windows local disk
232+
- See [docs-md/openshift-deployment/BACKUP_TO_NETWORK_SHARE.md](../docs-md/openshift-deployment/BACKUP_TO_NETWORK_SHARE.md) for the longer write-up
233+
234+
---
235+
209236
### oc-restore-db.sh — Database Restore
210237

211238
Restores a PostgreSQL database from a local SQL dump file into any instance.

scripts/artifactory-cleanup.sh

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,11 @@ fi
146146
if [[ -n "${KEEP_N}" ]]; then
147147
log_info "Rotation: keeping ${KEEP_N} most-recent '${MATCH_GLOB}' tags per image..."
148148

149+
# Match both `manifest.json` (legacy single-platform) and `list.manifest.json`
150+
# (OCI image index — what buildx writes for multi-platform pushes).
149151
ROTATION_AQL=$(curl -sf -u "${AUTH}" -X POST "${BASE_URL}/api/search/aql" \
150152
-H "Content-Type: text/plain" \
151-
-d "items.find({\"repo\":\"${ARTIFACTORY_REPO}\",\"type\":\"file\",\"name\":\"manifest.json\"}).include(\"repo\",\"path\",\"name\",\"created\")" 2>&1) || {
153+
-d "items.find({\"repo\":\"${ARTIFACTORY_REPO}\",\"type\":\"file\",\"\$or\":[{\"name\":\"manifest.json\"},{\"name\":\"list.manifest.json\"}]}).include(\"repo\",\"path\",\"name\",\"created\")" 2>&1) || {
152154
log_error "Rotation AQL query failed."
153155
exit 1
154156
}
@@ -160,21 +162,31 @@ keep = int('${KEEP_N}')
160162
glob = '${MATCH_GLOB}'
161163
162164
# Each result has path like 'backend-services/bcgov-di-abc123' (the tag dir)
163-
# plus name='manifest.json'. Extract (image, tag, created).
165+
# plus name like 'manifest.json' or 'list.manifest.json'. A tag may have both;
166+
# dedupe by (image, tag), keeping the newest created timestamp.
164167
by_image = {}
168+
seen = {}
165169
for r in data['results']:
166170
parts = r['path'].split('/')
167171
if len(parts) < 2:
168172
continue
169-
image = parts[0]
170-
tag = parts[1]
173+
image, tag = parts[0], parts[1]
171174
if tag.startswith('sha256__') or tag.startswith('sha256:') or tag == '_uploads':
172175
continue
173176
if not fnmatch.fnmatch(tag, glob):
174177
continue
175-
by_image.setdefault(image, []).append({'tag': tag, 'created': r.get('created', '')})
178+
created = r.get('created', '')
179+
key = (image, tag)
180+
if key in seen:
181+
if created > seen[key]:
182+
seen[key] = created
183+
continue
184+
seen[key] = created
185+
by_image.setdefault(image, []).append({'tag': tag, 'key': key})
176186
177187
for image, tags in by_image.items():
188+
for t in tags:
189+
t['created'] = seen[t['key']]
178190
tags.sort(key=lambda t: t['created'], reverse=True)
179191
for t in tags[keep:]:
180192
print(f\"{image}\t{t['tag']}\")

0 commit comments

Comments
 (0)