Skip to content

Commit

Permalink
ci: prune docker tags prefixed with "master-" older than 90 days on m…
Browse files Browse the repository at this point in the history
…erge (#1639)
  • Loading branch information
rfratto authored Feb 5, 2020
1 parent 375fc86 commit 1fe5dc4
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .drone/drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,24 @@ local manifest(apps) = pipeline('manifest') {
},
],
},
] + [
pipeline('prune-ci-tags') {
trigger: condition('include').tagMaster,
depends_on: ['manifest'],
steps: [
{
name: 'trigger',
image: 'grafana/loki-build-image:%s' % build_image_version,
environment: {
DOCKER_USERNAME: { from_secret: 'docker_username' },
DOCKER_PASSWORD: { from_secret: 'docker_password' },
},
commands: [
'go run ./tools/delete_tags.go -max-age=2160h -repo grafana/loki -delete',
'go run ./tools/delete_tags.go -max-age=2160h -repo grafana/promtail -delete',
'go run ./tools/delete_tags.go -max-age=2160h -repo grafana/loki-canary -delete',
],
},
],
},
]
29 changes: 29 additions & 0 deletions .drone/drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -576,4 +576,33 @@ trigger:
depends_on:
- manifest

---
kind: pipeline
name: prune-ci-tags

platform:
os: linux
arch: amd64

steps:
- name: trigger
image: grafana/loki-build-image:0.9.1
commands:
- go run ./tools/delete_tags.go -max-age=2160h -repo grafana/loki -delete
- go run ./tools/delete_tags.go -max-age=2160h -repo grafana/promtail -delete
- go run ./tools/delete_tags.go -max-age=2160h -repo grafana/loki-canary -delete
environment:
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_USERNAME:
from_secret: docker_username

trigger:
ref:
- refs/heads/master
- refs/tags/v*

depends_on:
- manifest

...
197 changes: 197 additions & 0 deletions tools/delete_tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)

type auth struct {
Username string `json:"username"`
Password string `json:"password"`
}

func main() {
var (
auth auth

repo string
maxAge time.Duration
filter string
doDelete bool
)

flag.BoolVar(&doDelete, "delete", false, "turn deletions on")
flag.StringVar(&auth.Username, "username", "", "username for docker hub")
flag.StringVar(&auth.Password, "password", "", "password for docker hub")
flag.StringVar(&repo, "repo", "grafana/loki", "repo to delete tags for")
flag.StringVar(&filter, "filter", "master-", "delete tags only containing this name")
flag.DurationVar(&maxAge, "max-age", 24*time.Hour*90, "delete tags older than this age")
flag.Parse()

if username := os.Getenv("DOCKER_USERNAME"); username != "" {
auth.Username = username
}
if password := os.Getenv("DOCKER_PASSWORD"); password != "" {
auth.Password = password
}

log.Printf("Using repo %s\n", repo)
log.Printf("Using search filter %s\n", filter)
log.Printf("Using max age %v\n", maxAge)

// Get an auth token
jwt, err := getJWT(auth)
if err != nil {
log.Fatalln(err)
}

tags, err := getTags(jwt, repo)
if err != nil {
log.Fatalln(err)
}

log.Printf("Discovered %d tags pre-filtering\n", len(tags))

filtered := make([]tag, 0, len(tags))

for _, t := range tags {
if !strings.Contains(t.Name, filter) {
continue
}
age := time.Since(t.LastUpdated)
if age < maxAge {
continue
}

filtered = append(filtered, t)
}

if !doDelete {
log.Printf("Should delete %d tags\n", len(filtered))
for _, t := range filtered {
fmt.Printf("%s: last updated %s\n", t.Name, t.LastUpdated)
}
} else {
log.Printf("Deleting %d tags\n", len(filtered))
for _, t := range filtered {
log.Printf("Deleting %s (last updated %s)\n", t.Name, t.LastUpdated)
if err := deleteTag(jwt, repo, t.Name); err != nil {
log.Printf("Failed to delete %s: %v", t.Name, err)
}
}
}
}

func getJWT(a auth) (string, error) {
body, err := json.Marshal(a)
if err != nil {
return "", err
}

loginURL := "https://hub.docker.com/v2/users/login"
resp, err := http.Post(loginURL, "application/json", bytes.NewReader(body))
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
resp.Body.Close()
log.Fatalf("failed to log in: %v", string(body))
}
defer resp.Body.Close()

m := map[string]interface{}{}
err = json.NewDecoder(resp.Body).Decode(&m)
if err != nil {
return "", err
}

return m["token"].(string), nil
}

type tag struct {
Name string `json:"name"`
LastUpdated time.Time `json:"last_updated"`
}

type getTagResponse struct {
NextURL *string `json:"next"`
Results []tag `json:"results"`
}

func getTags(jwt string, repo string) ([]tag, error) {
var tags []tag

tagsURL := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags", repo)
res, err := getTagsFromURL(jwt, tagsURL)
if err != nil {
return nil, err
}
tags = append(tags, res.Results...)

for res.NextURL != nil {
res, err = getTagsFromURL(jwt, *res.NextURL)
if err != nil {
return nil, err
}
tags = append(tags, res.Results...)
}

return tags, nil
}

func getTagsFromURL(jwt string, url string) (getTagResponse, error) {
var res getTagResponse

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return res, err
}
req.Header.Add("Authorization", "JWT "+jwt)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return res, err
}
if resp.StatusCode != 200 {
return res, errors.New("failed to get tags")
}
defer resp.Body.Close()

err = json.NewDecoder(resp.Body).Decode(&res)
return res, err
}

func deleteTag(jwt string, repo string, tag string) error {
tagsURL := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/tags/%s/", repo, tag)
req, err := http.NewRequest(http.MethodDelete, tagsURL, nil)
if err != nil {
return err
}
req.Header.Add("Authorization", "JWT "+jwt)

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

bb, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode/100 != 2 {
return fmt.Errorf("resp code %d: %s", resp.StatusCode, string(bb))
}

return err
}

0 comments on commit 1fe5dc4

Please sign in to comment.