Operations 18 min read

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.

Ops Development Stories
Ops Development Stories
Ops Development Stories
Automate Harbor Image Tag Cleanup with a Go CLI Script

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)), &amp;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)), &amp;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), &amp;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, &amp;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

cliGoDevOpsTag ManagementHarborImage Cleanup
Ops Development Stories
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.