Skip to content

Commit b7fbc50

Browse files
authored
fix(ci): ns8-ci cleanup, use curl
Drop doctl in favor of curl, because doctl does not work well inside GitHub actions
1 parent 2f0faa4 commit b7fbc50

File tree

3 files changed

+88
-63
lines changed

3 files changed

+88
-63
lines changed

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

Lines changed: 0 additions & 25 deletions
This file was deleted.

.github/workflows/ns8-ci.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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-cleanup:
11+
name: Remove unused NS8 CI resources
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v5
16+
17+
- name: Execute ns8-ci.sh
18+
run: ./scripts/ns8-ci.sh
19+
env:
20+
DIGITALOCEAN_TOKEN: ${{ secrets.NS8_CI_DIGITALOCEAN_TOKEN }}
Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,61 @@
11
#!/usr/bin/env bash
22

33
# DigitalOcean NS8-CI cleanup candidate lister (and optional deleter)
4-
# Requirements: doctl, jq
4+
# Requirements: curl, jq
5+
# This script does not use doctl because it does not run well inside GitHub Actions due
6+
# to lack of a TTY. Instead, it uses direct API calls with curl.
57
#
68
# Usage:
7-
# ./doctl-ns8-ci.sh # just list
8-
# ./doctl-ns8-ci.sh --delete # list and delete
9+
# ./ns8-ci.sh # just list
10+
# ./ns8-ci.sh --delete # list and delete
911

1012
DO_DOMAIN="ci.nethserver.net"
1113
TAG_PREFIX="NS8-CI-"
1214
DELETE=0
15+
DO_API_BASE="https://api.digitalocean.com/v2"
16+
17+
18+
# Function to make authenticated API calls to DigitalOcean
19+
do_api() {
20+
local method="${1:-GET}"
21+
local endpoint="$2"
22+
local data="$3"
23+
24+
local curl_args=(
25+
-s
26+
-H "Authorization: Bearer $DIGITALOCEAN_ACCESS_TOKEN"
27+
-H "Content-Type: application/json"
28+
-X "$method"
29+
)
30+
31+
if [[ -n "$data" ]]; then
32+
curl_args+=(-d "$data")
33+
fi
34+
35+
# Check if the endpoint already contains a '?'
36+
if [[ "$endpoint" == *\?* ]]; then
37+
curl "${curl_args[@]}" "$DO_API_BASE$endpoint&page=1&per_page=200"
38+
else
39+
curl "${curl_args[@]}" "$DO_API_BASE$endpoint?page=1&per_page=200"
40+
fi
41+
}
1342

1443
if [[ "$1" == "--delete" ]]; then
1544
DELETE=1
1645
fi
1746

1847
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"
2248

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."
49+
# Get the DigitalOcean access token from environment
50+
if [[ -z "$DIGITALOCEAN_ACCESS_TOKEN" ]]; then
51+
echo "DigitalOcean access token not found. Please set one of:"
52+
echo " DIGITALOCEAN_TOKEN, DIGITALOCEAN_ACCESS_TOKEN, or DOCTL_ACCESS_TOKEN"
53+
exit 1
54+
fi
55+
56+
# Check if curl is installed
57+
if ! command -v curl &> /dev/null; then
58+
echo "curl could not be found. Please install curl."
2659
exit 1
2760
fi
2861

@@ -32,50 +65,44 @@ if ! command -v jq &> /dev/null; then
3265
exit 1
3366
fi
3467

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
68+
# Test API access by getting account info
69+
if ! do_api GET "/account" | jq -e '.account' &> /dev/null; then
70+
echo "Failed to authenticate with DigitalOcean API. Please check your token."
71+
exit 1
4772
fi
4873

4974
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)
75+
# Get all tags and filter for NS8-CI prefix
76+
mapfile -t ns8_tags < <(do_api GET "/tags" | jq -r '.tags[] | select(.name | startswith("'$TAG_PREFIX'")) | .name')
77+
5178
# Remove unused tags (no droplets attached) as a first step
5279
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)
80+
# Get droplets with this tag
81+
mapfile -t tag_droplets_check < <(do_api GET "/droplets?tag_name=$tag" | jq -r '.droplets[] | .id')
5482
if [[ ${#tag_droplets_check[@]} -eq 0 ]]; then
5583
echo "Unused tag: $tag"
5684
if [[ $DELETE -eq 1 ]]; then
5785
echo "-> Deleting tag $tag"
58-
$doctl_cmd compute tag delete "$tag" -f || true
86+
do_api DELETE "/tags/$tag" || true
5987
fi
6088
fi
6189
done
6290

6391
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)
92+
# Get all tags with NS8-CI prefix again (in case some were deleted)
93+
mapfile -t ns8_tags < <(do_api GET "/tags" | jq -r '.tags[] | select(.name | startswith("'$TAG_PREFIX'")) | .name')
6594
droplet_ids=()
6695
droplet_names=()
6796
# threshold in seconds (3 hours)
6897
THRESHOLD_SECONDS=10800
6998
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)"')
99+
# Get droplets with this tag
100+
mapfile -t tag_droplets < <(do_api GET "/droplets?tag_name=$tag" | jq -r '.droplets[] | "\(.id) \(.status) \(.created_at) \(.name)"')
72101
for entry in "${tag_droplets[@]}"; do
73102
id=$(echo "$entry" | awk '{print $1}')
74103
status=$(echo "$entry" | awk '{print $2}')
75104
created_at=$(echo "$entry" | awk '{print $3}')
76105
name=$(echo "$entry" | cut -d' ' -f4-)
77-
echo "$entry"
78-
echo "id=$id status=$status created_at=$created_at name=$name"
79106

80107
# Skip if we couldn't parse fields
81108
if [[ -z "$id" || -z "$created_at" ]]; then
@@ -104,15 +131,16 @@ for tag in "${ns8_tags[@]}"; do
104131
echo "Droplet: $name ($id) [tag: $tag] status=$status age=${age_hours}h"
105132
if [[ $DELETE -eq 1 ]]; then
106133
echo "-> Deleting droplet $name ($id)"
107-
$doctl_cmd compute droplet delete "$id" -f
134+
do_api DELETE "/droplets/$id"
108135
fi
109136
fi
110137
done
111138
done
112139

113140
echo ""
114141
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)
142+
# Get all DNS records for the domain
143+
mapfile -t records < <(do_api GET "/domains/$DO_DOMAIN/records" | jq -r '.domain_records[] | "\(.id) \(.type) \(.name)"')
116144
for record in "${records[@]}"; do
117145
id=$(echo "$record" | awk '{print $1}')
118146
type=$(echo "$record" | awk '{print $2}')
@@ -129,24 +157,26 @@ for record in "${records[@]}"; do
129157
echo "Orphan DNS $type record: $name.$DO_DOMAIN (record id: $id)"
130158
if [[ $DELETE -eq 1 ]]; then
131159
echo "-> Deleting DNS record $id ($name.$DO_DOMAIN)"
132-
$doctl_cmd compute domain records delete "$DO_DOMAIN" "$id" -f
160+
do_api DELETE "/domains/$DO_DOMAIN/records/$id"
133161
fi
134162
fi
135163
fi
136164
done
137165

138166
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)
167+
echo "== 4. SSH keys with names matching '*.ci.nethserver.net' not used by any droplet =="
168+
# Get SSH keys matching '.ci.nethserver.net'
169+
mapfile -t ssh_keys_raw < <(do_api GET "/account/keys" | jq -r '.ssh_keys[] | select(.name | contains(".ci.nethserver.net")) | "\(.id) \(.name)"')
170+
# Get all droplet IDs for checking SSH key usage
171+
mapfile -t all_droplet_ids < <(do_api GET "/droplets" | jq -r '.droplets[] | .id')
143172

144173
for ssh_entry in "${ssh_keys_raw[@]}"; do
145174
ssh_id=$(echo "$ssh_entry" | awk '{print $1}')
146175
ssh_name=$(echo "$ssh_entry" | cut -d' ' -f2-)
147176
ssh_used=0
148177
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}')
178+
# Get droplet details to check SSH keys
179+
mapfile -t droplet_keys < <(do_api GET "/droplets/$droplet_id" | jq -r '.droplet.ssh_keys[]? | .id')
150180
for dkey in "${droplet_keys[@]}"; do
151181
if [[ "$dkey" == "$ssh_id" ]]; then
152182
ssh_used=1
@@ -158,7 +188,7 @@ for ssh_entry in "${ssh_keys_raw[@]}"; do
158188
echo "Unused SSH key: $ssh_name ($ssh_id)"
159189
if [[ $DELETE -eq 1 ]]; then
160190
echo "-> Deleting SSH key $ssh_name ($ssh_id)"
161-
$doctl_cmd compute ssh-key delete "$ssh_id" -f
191+
do_api DELETE "/account/keys/$ssh_id"
162192
fi
163193
fi
164194
done

0 commit comments

Comments
 (0)