Automate Harbor Image Tag Cleanup with a Go CLI Script
This article explains how to create a Go‑based command‑line tool that queries Harbor projects and repositories, lists image tags, and automatically deletes older tags while retaining a configurable number of recent ones, streamlining storage management and avoiding manual page‑by‑page deletions.
Background
During a routine inspection the Harbor registry showed 80% storage usage; some projects contained hundreds of image tags because they were in a debugging phase. Manually deleting tags page by page was tedious, so a script was written in Go to keep only the most recent tags.
Goal
List all projects, regardless of visibility, and their repository counts via CLI.
List repository names and pull counts for a given project.
Delete image tags by specifying a retention count.
Retrieve the number of tags for an image.
Note that garbage collection must be run manually after deletion.
Declaration
The script is for personal practice only and does not constitute advice.
Written by a Go beginner; code quality is poor.
Harbor version used is v2.3.1.
Full source code is available on GitHub.
Implementation
Get all projects using Harbor’s Swagger API.
<code>// Define data structures based on Harbor Swagger results
type MetaData struct {
Public string `json:"public"`
}
type ProjectData struct {
MetaData MetaData `json:"metadata"`
ProjectId int `json:"project_id"`
Name string `json:"name"`
RepoCount int `json:"repo_count"`
}
type PData []ProjectData
// GetProject fetches projects from the given Harbor URL
func GetProject(url string) []map[string]string {
url = url + "/api/v2.0/projects"
request, _ := http.NewRequest(http.MethodGet, url, nil)
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Timeout: 10 * time.Second, Transport: tr}
request.Header.Set("accept", "application/json")
request.SetBasicAuth("admin", "Harbor12345")
response, err := client.Do(request)
if err != nil {
fmt.Println("execute failed")
fmt.Println(err)
}
body, _ := ioutil.ReadAll(response.Body)
defer response.Body.Close()
ret := PData{}
json.Unmarshal([]byte(string(body)), &ret)
var ps = []map[string]string{}
for i := 0; i < len(ret); i++ {
rdata := make(map[string]string)
rdata["name"] = ret[i].Name
rdata["project_id"] = strconv.Itoa(ret[i].ProjectId)
rdata["repo_count"] = strconv.Itoa(ret[i].RepoCount)
rdata["public"] = ret[i].MetaData.Public
ps = append(ps, rdata)
}
return ps
}
</code>Get repositories under a project.
<code>type ReposiData struct {
Id int `json:"id"`
Name string `json:"name"`
ProjectId int `json:"project_id"`
PullCount int `json:"pull_count"`
}
type RepoData []ReposiData
func GetRepoData(url string, proj string) []map[string]string {
url = url + "/api/v2.0/projects/" + proj + "/repositories"
request, _ := http.NewRequest(http.MethodGet, url, nil)
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Timeout: 10 * time.Second, Transport: tr}
request.Header.Set("accept", "application/json")
request.SetBasicAuth("admin", "Harbor12345")
response, err := client.Do(request)
if err != nil {
fmt.Println("execute failed")
fmt.Println(err)
}
body, _ := ioutil.ReadAll(response.Body)
defer response.Body.Close()
ret := RepoData{}
json.Unmarshal([]byte(string(body)), &ret)
var ps = []map[string]string{}
for i := 0; i < len(ret); i++ {
rdata := make(map[string]string)
rdata["name"] = ret[i].Name
rdata["pullCount"] = strconv.Itoa(ret[i].PullCount)
ps = append(ps, rdata)
}
return ps
}
</code>Tag operations: list, sort by push time, and delete.
<code>// Tag data structures
type Tag struct {
ArtifactId int `json:"artifact_id"`
Id int `json:"id"`
Name string `json:"name"`
RepositoryId int `json:"repository_id"`
PushTimte string `json:"push_time"`
}
type Tag2 struct {
ArtifactId string `json:"artifact_id"`
Id string `json:"id"`
Name string `json:"name"`
RepositoryId string `json:"repository_id"`
PushTimte string `json:"push_time"`
}
type Tag2s []Tag2
// Delete tags by count (keeps the newest ones)
func DeleTagsByCount(tags []map[string]string, count int) []string {
var re []string
tt := tags[0]["tags"]
ss := Tag2s{}
json.Unmarshal([]byte(tt), &ss)
// sort by push time
for i := 0; i < len(ss); i++ {
for j := i + 1; j < len(ss); j++ {
if ss[i].PushTimte > ss[j].PushTimte {
ss[i], ss[j] = ss[j], ss[i]
}
}
}
for i := 0; i < len(ss); i++ {
re = append(re, ss[i].Name)
}
return re[0:count]
}
// Delete a specific tag
func DelTags(url string, project string, repo string, tag string) (int, map[string]interface{}) {
url = url + "/api/v2.0/projects/" + project + "/repositories/" + repo + "/artifacts/" + tag + "/tags/" + tag
request, _ := http.NewRequest(http.MethodDelete, url, nil)
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{Timeout: 10 * time.Second, Transport: tr}
request.Header.Set("accept", "application/json")
request.SetBasicAuth("admin", "Pwd123456")
response, _ := client.Do(request)
defer response.Body.Close()
var result map[string]interface{}
bd, err := ioutil.ReadAll(response.Body)
if err == nil {
json.Unmarshal(bd, &result)
}
return response.StatusCode, result
}
</code>Test
<code>// Help
harbor % ./harbor -h https://harbor.example.com
// List all projects
./harbor project ls -u https://harbor.example.com
// List all repositories in a project
./harbor repo ls -u https://harbor.example.com -p goharbor
// List tags of a repository
./harbor tag ls -u https://harbor.example.com -p goharbor -r harbor-db
// Delete tags, keeping the latest N tags
./harbor tag del -u https://harbor.example.com -p goharbor -r harbor-db -c 5
// Delete a specific tag
./harbor tag del -u https://harbor.example.com -p goharbor -r harbor-db -t v2.3.6
// Remember to run garbage collection in the Harbor UI after deletions.
</code>Reference
https://github.com/spf13/cobra
Harbor Swagger API documentation
Ops Development Stories
Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.