diff --git a/.drone/drone.jsonnet b/.drone/drone.jsonnet index a43b131c28498..69f3ccc3a1f27 100644 --- a/.drone/drone.jsonnet +++ b/.drone/drone.jsonnet @@ -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', + ], + }, + ], + }, ] diff --git a/.drone/drone.yml b/.drone/drone.yml index d159080238a81..ca0df5594bd73 100644 --- a/.drone/drone.yml +++ b/.drone/drone.yml @@ -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 + ... diff --git a/tools/delete_tags.go b/tools/delete_tags.go new file mode 100644 index 0000000000000..5bb468550572f --- /dev/null +++ b/tools/delete_tags.go @@ -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 +}