Skip to content

Commit faac4a1

Browse files
authored
Merge pull request #1954 from GoogleContainerTools/repo_tools
Add bulk tagger
2 parents 3b1ffa0 + c50abdd commit faac4a1

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

repo_tools/tag.sh

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
trap 'echo "Ctrl+C caught, exiting..."; exit 130' INT
5+
6+
function exec_tag() {
7+
local image=$1
8+
local tag_prefix=$2
9+
local -n hashes=$3
10+
local count=0
11+
local total="${#hashes[@]}"
12+
for h in "${hashes[@]}"; do
13+
local sha256="${h#sha256:}"
14+
local img_path="${image}@${h}"
15+
local tag="${tag_prefix}${sha256}"
16+
echo -n "tagging $img_path with $tag "
17+
if [[ ${dry_run} == false ]]; then
18+
gcrane tag "$img_path" "$tag" && echo "SUCCESS $((++count))/$total" || { echo "FAILED"; exit 1; }
19+
else
20+
echo "DRY_RUN"
21+
fi
22+
done
23+
}
24+
25+
function tag_update() {
26+
local tag_prefix="update-available-"
27+
local image="$1"
28+
29+
local NOW
30+
local images_json
31+
32+
NOW=$(date +%s000)
33+
images_json=$(gcrane ls "$image" --json)
34+
35+
# get all hashes for images that have no tags (or just the commit hash tag)
36+
# jq behavior for all(test(pattern)) matches the empty list []
37+
# or are older than 2 days (we don't want to accidentally mess with any
38+
# ongoing builds)
39+
readarray -t targets < <(echo "$images_json" | jq -er --arg now "$NOW" '
40+
.manifest | to_entries | sort_by(.value.timeUploadedMs|tonumber) | .[] | select(
41+
(.value.tag // [] | all(test(".*-[a-f0-9]{40}$|^[a-f0-9]{40}$")))
42+
and
43+
(($now | tonumber) - (.value.timeUploadedMs | tonumber) > 172800000)
44+
) | .key
45+
');
46+
47+
echo "tagging ${#targets[@]} images of $image"
48+
49+
exec_tag "$image" "$tag_prefix" targets
50+
}
51+
52+
function tag_deprecate() {
53+
local targets
54+
local tag_prefix="deprecated-public-image-"
55+
local image=$1
56+
57+
local images_json
58+
images_json=$(gcrane ls "$image" --json)
59+
60+
# get all hashes for all images don't have the deprecated tag
61+
readarray -t targets < <(echo "$images_json" | jq -er '
62+
.manifest | to_entries | sort_by(.value.timeUploadedMs|tonumber) | .[] | select(
63+
.value.tag // [] | all(test("deprecated-public-image-[a-f0-9]{64}$") | not)
64+
) | .key
65+
');
66+
67+
echo "tagging ${#targets[@]} images of $image"
68+
echo "disabled for now, edit out comment"
69+
# exec_tag "$image" "$tag_prefix" targets
70+
}
71+
72+
dry_run=${DRY_RUN:-true}
73+
if [[ "${dry_run}" != "true" && "${dry_run}" != "false" ]]; then
74+
echo "Error: DRY_RUN needs to have value (true|false)."
75+
exit 1
76+
fi
77+
78+
function tag() {
79+
local tag_mode=$1
80+
local image=$2
81+
case "$tag_mode" in
82+
update)
83+
tag_update "$image"
84+
;;
85+
deprecate)
86+
tag_deprecate "$image"
87+
;;
88+
*)
89+
echo "wtf?"
90+
exit 1
91+
esac
92+
}
93+
94+
function usage() {
95+
echo "USAGE: $1 [deprecate|update] <image>" >&2
96+
exit 1
97+
}
98+
99+
case "${1:-"~~nocmd"}" in
100+
update | deprecate)
101+
if [[ "$#" -ne 2 ]]; then
102+
usage "$0"
103+
fi
104+
tag "$@"
105+
;;
106+
~~nocmd | *)
107+
usage "$0"
108+
;;
109+
esac

repo_tools/tag.sh.README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Container Image Lifecycle Tagger (Distroless Admin Tooling)
2+
3+
A Bash utility to bulk-tag container images in a registry. This is particularly useful for marking old images as "available for update" or "deprecated" without deleting them, allowing for safer cleanup or auditing.
4+
5+
## Prerequisites
6+
7+
* **[gcrane](https://github.com/google/go-containerregistry/tree/main/cmd/gcrane):** Used for interacting with the container registry.
8+
* **[jq](https://github.com/jqlang/jq/):** Used for parsing registry JSON manifests.
9+
* **Authentication:** Ensure you are authenticated to your registry (e.g., `gcloud auth configure-docker`).
10+
11+
## Functionality
12+
13+
The script operates in two primary modes:
14+
15+
### 1. `update` mode
16+
17+
Identifies images that are "stale" but likely safe to mark for replacement. It selects images that:
18+
19+
* Have no tags **OR** only have auto-generated git commit hash based tags (40-character hex strings).
20+
* Are older than **48 hours** (to avoid interfering with active CI/CD pipelines).
21+
* **Action:** Tags them with the prefix `update-available-<sha256>`.
22+
23+
### 2. `deprecate` mode
24+
25+
Identifies images that have not yet been marked as deprecated.
26+
27+
* Selects images that do **not** already have a tag matching the deprecation pattern.
28+
* **Action:** Tags them with the prefix `deprecated-public-image-<sha256>`.
29+
* *Note: The execution for this mode is currently commented out in the script as a safety measure.*
30+
31+
---
32+
33+
## Usage
34+
35+
```bash
36+
./tag.sh [update|deprecate] <image>
37+
```
38+
39+
### Examples
40+
41+
**Dry Run (Default):**
42+
By default, the script only prints what it *would* do.
43+
44+
```bash
45+
./tag.sh update gcr.io/distroless/python3-debian12
46+
```
47+
48+
**Live Execution:**
49+
To actually apply tags to the registry, set `DRY_RUN` to `false`.
50+
51+
```bash
52+
DRY_RUN=false ./tag.sh update gcr.io/distroless/python3-debian12
53+
```

0 commit comments

Comments
 (0)