Skip to content

Commit 2f0faa4

Browse files
committed
feat(ci): add cleanup script (#7621)
Make sure to remove unused NS8 CI resources from Digital Ocean. The Github Action does only listing for now.
1 parent cf5c2e4 commit 2f0faa4

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

.github/workflows/doctl-ns8-ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Cleanup NS8 CI
2+
3+
on:
4+
schedule:
5+
# Runs every night at 04:00 UTC
6+
- cron: '0 4 * * *'
7+
workflow_dispatch:
8+
9+
jobs:
10+
run-doctl:
11+
name: Execute doctl-ns8-ci.sh --delete
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v5
16+
17+
- name: Install doctl and authenticate
18+
uses: digitalocean/action-doctl@v2
19+
with:
20+
token: ${{ secrets.NS8_CI_DIGITALOCEAN_TOKEN }}
21+
22+
- name: Remove unused NS8 CI resources
23+
run: ./scripts/doctl-ns8-ci.sh
24+
env:
25+
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.NS8_CI_DIGITALOCEAN_TOKEN }}

scripts/doctl-ns8-ci.sh

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/usr/bin/env bash
2+
3+
# DigitalOcean NS8-CI cleanup candidate lister (and optional deleter)
4+
# Requirements: doctl, jq
5+
#
6+
# Usage:
7+
# ./doctl-ns8-ci.sh # just list
8+
# ./doctl-ns8-ci.sh --delete # list and delete
9+
10+
DO_DOMAIN="ci.nethserver.net"
11+
TAG_PREFIX="NS8-CI-"
12+
DELETE=0
13+
14+
if [[ "$1" == "--delete" ]]; then
15+
DELETE=1
16+
fi
17+
18+
set -e
19+
# Default $doctl_cmd context (can be overridden by exporting DOCTL_CONTEXT)
20+
DOCTL_CONTEXT="${DOCTL_CONTEXT:-sviluppo}"
21+
doctl_cmd="doctl --context $DOCTL_CONTEXT"
22+
23+
# Check if doctl is installed
24+
if ! command -v doctl &> /dev/null; then
25+
echo "doctl could not be found. Please install doctl and configure it with access token."
26+
exit 1
27+
fi
28+
29+
# Check if jq is installed
30+
if ! command -v jq &> /dev/null; then
31+
echo "jq could not be found. Please install jq."
32+
exit 1
33+
fi
34+
35+
36+
# Check if doctl can access DigitalOcean
37+
if ! $doctl_cmd account get &> /dev/null; then
38+
if [[ -z "$DIGITALOCEAN_ACCESS_TOKEN" ]]; then
39+
echo "doctl could not access DigitalOcean. Please use 'doctl auth init' to configure it or set the DIGITALOCEAN_ACCESS_TOKEN environment variable."
40+
exit 1
41+
fi
42+
echo $DIGITALOCEAN_ACCESS_TOKEN | $doctl_cmd auth init --interactive false
43+
if ! $doctl_cmd account get &> /dev/null; then
44+
echo "Auth failed."
45+
exit 1
46+
fi
47+
fi
48+
49+
echo "== 1. Unused tags starting with $TAG_PREFIX =="
50+
mapfile -t ns8_tags < <($doctl_cmd compute tag list --format Name --no-header | grep "^$TAG_PREFIX" || true)
51+
# Remove unused tags (no droplets attached) as a first step
52+
for tag in "${ns8_tags[@]}"; do
53+
mapfile -t tag_droplets_check < <($doctl_cmd compute droplet list --tag-name "$tag" --format ID --no-header || true)
54+
if [[ ${#tag_droplets_check[@]} -eq 0 ]]; then
55+
echo "Unused tag: $tag"
56+
if [[ $DELETE -eq 1 ]]; then
57+
echo "-> Deleting tag $tag"
58+
$doctl_cmd compute tag delete "$tag" -f || true
59+
fi
60+
fi
61+
done
62+
63+
echo "== 2. Droplets with tags starting with $TAG_PREFIX (only 'active' and running > 3h) =="
64+
mapfile -t ns8_tags < <($doctl_cmd compute tag list --format Name --no-header | grep "^$TAG_PREFIX" || true)
65+
droplet_ids=()
66+
droplet_names=()
67+
# threshold in seconds (3 hours)
68+
THRESHOLD_SECONDS=10800
69+
for tag in "${ns8_tags[@]}"; do
70+
# Request fields via JSON so we reliably get created_at; parse with jq to: ID Status CreatedAt Name
71+
mapfile -t tag_droplets < <($doctl_cmd compute droplet list --tag-name "$tag" -o json | jq -r '.[] | "\(.id) \(.status) \(.created_at) \(.name)"')
72+
for entry in "${tag_droplets[@]}"; do
73+
id=$(echo "$entry" | awk '{print $1}')
74+
status=$(echo "$entry" | awk '{print $2}')
75+
created_at=$(echo "$entry" | awk '{print $3}')
76+
name=$(echo "$entry" | cut -d' ' -f4-)
77+
echo "$entry"
78+
echo "id=$id status=$status created_at=$created_at name=$name"
79+
80+
# Skip if we couldn't parse fields
81+
if [[ -z "$id" || -z "$created_at" ]]; then
82+
continue
83+
fi
84+
85+
# Only consider active droplets
86+
if [[ "$status" != "active" ]]; then
87+
continue
88+
fi
89+
90+
# Parse created_at to epoch and compute age
91+
created_epoch=$(date -d "$created_at" +%s 2>/dev/null || true)
92+
if [[ -z "$created_epoch" ]]; then
93+
# fallback: skip if date parsing fails
94+
continue
95+
fi
96+
now_epoch=$(date +%s)
97+
age=$(( now_epoch - created_epoch ))
98+
99+
if (( age > THRESHOLD_SECONDS )); then
100+
droplet_ids+=("$id")
101+
droplet_names+=("$name")
102+
# show human-friendly age in hours (with integer hours)
103+
age_hours=$(( age / 3600 ))
104+
echo "Droplet: $name ($id) [tag: $tag] status=$status age=${age_hours}h"
105+
if [[ $DELETE -eq 1 ]]; then
106+
echo "-> Deleting droplet $name ($id)"
107+
$doctl_cmd compute droplet delete "$id" -f
108+
fi
109+
fi
110+
done
111+
done
112+
113+
echo ""
114+
echo "== 3. DNS records in $DO_DOMAIN without a running droplet =="
115+
mapfile -t records < <($doctl_cmd compute domain records list "$DO_DOMAIN" --format ID,Type,Name --no-header)
116+
for record in "${records[@]}"; do
117+
id=$(echo "$record" | awk '{print $1}')
118+
type=$(echo "$record" | awk '{print $2}')
119+
name=$(echo "$record" | awk '{print $3}')
120+
if [[ "$type" == "A" || "$type" == "AAAA" ]]; then
121+
found=0
122+
for dname in "${droplet_names[@]}"; do
123+
if [[ "$dname" == "$name" ]]; then
124+
found=1
125+
break
126+
fi
127+
done
128+
if [[ $found -eq 0 ]]; then
129+
echo "Orphan DNS $type record: $name.$DO_DOMAIN (record id: $id)"
130+
if [[ $DELETE -eq 1 ]]; then
131+
echo "-> Deleting DNS record $id ($name.$DO_DOMAIN)"
132+
$doctl_cmd compute domain records delete "$DO_DOMAIN" "$id" -f
133+
fi
134+
fi
135+
fi
136+
done
137+
138+
echo ""
139+
echo "== 3. SSH keys with names matching '^*ci.nethserver.net-deploy' not used by any droplet =="
140+
# Collect SSH keys matching '.ci.nethserver.net' using the same template you provided
141+
mapfile -t ssh_keys_raw < <($doctl_cmd compute ssh-key list --format ID,Name --no-header | grep '\.ci\.nethserver\.net' || true)
142+
mapfile -t all_droplet_ids < <($doctl_cmd compute droplet list --format ID --no-header)
143+
144+
for ssh_entry in "${ssh_keys_raw[@]}"; do
145+
ssh_id=$(echo "$ssh_entry" | awk '{print $1}')
146+
ssh_name=$(echo "$ssh_entry" | cut -d' ' -f2-)
147+
ssh_used=0
148+
for droplet_id in "${all_droplet_ids[@]}"; do
149+
mapfile -t droplet_keys < <($doctl_cmd compute droplet get "$droplet_id" --format SSHKeys --no-header | tr ',' '\n' | awk '{print $1}')
150+
for dkey in "${droplet_keys[@]}"; do
151+
if [[ "$dkey" == "$ssh_id" ]]; then
152+
ssh_used=1
153+
break 2
154+
fi
155+
done
156+
done
157+
if [[ $ssh_used -eq 0 ]]; then
158+
echo "Unused SSH key: $ssh_name ($ssh_id)"
159+
if [[ $DELETE -eq 1 ]]; then
160+
echo "-> Deleting SSH key $ssh_name ($ssh_id)"
161+
$doctl_cmd compute ssh-key delete "$ssh_id" -f
162+
fi
163+
fi
164+
done
165+
166+
echo ""
167+
if [[ $DELETE -eq 1 ]]; then
168+
echo "Done. All listed resources were deleted."
169+
else
170+
echo "Done. No resources were deleted (listing mode)."
171+
fi

0 commit comments

Comments
 (0)