diff --git a/.goreleaser.yml b/.goreleaser.yml index ebfe1a8..32dbc73 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,12 +2,12 @@ builds: - binary: dhlm env: - CGO_ENABLED=0 -archive: - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 snapshot: name_template: "{{ .Tag }}-next" \ No newline at end of file diff --git a/README.md b/README.md index e948992..46a840a 100644 --- a/README.md +++ b/README.md @@ -17,5 +17,6 @@ GLOBAL OPTIONS: --username value --password value --days value (default: "30") + --dry-run --help, -h show help ``` diff --git a/dhlm.go b/dhlm.go index ebf9774..576ba2d 100644 --- a/dhlm.go +++ b/dhlm.go @@ -20,20 +20,25 @@ func main() { var dhOrg string var dhRepo string var days string + var dryRun bool - app.Flags = []cli.Flag { + app.Flags = []cli.Flag{ cli.StringFlag{ - Name: "username", + Name: "username", Destination: &dhUsername, }, cli.StringFlag{ - Name: "password", + Name: "password", Destination: &dhPassword, }, cli.StringFlag{ - Name: "days", + Name: "days", Destination: &days, - Value: "30", + Value: "30", + }, + cli.BoolFlag{ + Name: "dry-run", + Destination: &dryRun, }, } @@ -47,25 +52,43 @@ func main() { return nil } - dh := dockerhub.NewClient(dockerhub.Auth { + dh := dockerhub.NewClient(dockerhub.Auth{ Username: dhUsername, Password: dhPassword, }) daysInt, _ := strconv.Atoi(days) - timeBefore := time.Now().Add(-time.Hour*24*time.Duration(daysInt)) + timeBefore := time.Now().Add(-time.Hour * 24 * time.Duration(daysInt)) pageNumber := 1 - for tagsList := dh.GetTags(dhOrg, dhRepo, pageNumber); len(tagsList.Next) > 0; pageNumber++ { + for tagsList := dh.GetImages(dhOrg, dhRepo, pageNumber, timeBefore); len(tagsList.Next) > 0; pageNumber++ { fmt.Println("Checking page:", pageNumber) + var digests []string + var ignoreList []*dockerhub.IgnoreWarnings for _, tag := range tagsList.Results { - if tag.LastUpdated.Unix() < timeBefore.Unix() { - fmt.Println("Removing "+dhOrg+"/"+dhRepo+":"+tag.Name+" | "+tag.LastUpdated.Format(time.RFC822)) - dh.DeleteTag(dhOrg, dhRepo, tag.Name) + if tag.LastPulled.Unix() < timeBefore.Unix() { + fmt.Println("Removing " + dhOrg + "/" + dhRepo + ":" + tag.Digest + " | " + tag.LastPulled.Format(time.RFC3339) + " | " + tag.LastPushed.Format(time.RFC3339)) + + digests = append(digests, tag.Digest) + + for _, t := range tag.Tags { + if t.IsCurrent == true { + ignoreList = append(ignoreList, &dockerhub.IgnoreWarnings{ + Repository: dhRepo, + Digest: tag.Digest, + Warning: "current_tag", + Tags: []string{t.Tag}, + }) + } + } + } } + deletedImages := dh.DeleteImages(dhOrg, dhRepo, digests, timeBefore, dryRun, ignoreList) + fmt.Printf("Summary of deleted images ➡ manifest_deletes: %d, manifest_errors: %d, tag_deletes: %d, tag_errors: %d \n", + deletedImages.Metrics.ManifestDeletes, deletedImages.Metrics.ManifestErrors, deletedImages.Metrics.TagDeletes, deletedImages.Metrics.TagDeletes) - tagsList = dh.GetTags(dhOrg, dhRepo, pageNumber) + tagsList = dh.GetImages(dhOrg, dhRepo, pageNumber, timeBefore) } return nil @@ -75,4 +98,4 @@ func main() { if err != nil { panic(err) } -} \ No newline at end of file +} diff --git a/dockerhub/dockerhub.go b/dockerhub/dockerhub.go index e195311..7886832 100644 --- a/dockerhub/dockerhub.go +++ b/dockerhub/dockerhub.go @@ -5,13 +5,17 @@ import ( "encoding/json" "io/ioutil" "net/http" + "net/url" "strconv" + "time" ) type client struct { token string } +const pageSize = "10" + func NewClient(auth Auth) *client { c := &client{} c.authorize(auth) @@ -46,24 +50,60 @@ func (client *client) authorize(auth Auth) { client.token = token.Token } -func (client *client) DeleteTag(organization string, repository string, tag string) { - req, err := http.NewRequest("DELETE", "https://hub.docker.com/v2/repositories/"+organization+"/"+repository+"/tags/"+tag+"/", nil) +func (client *client) DeleteImages(organization string, repository string, digests []string, timeBefore time.Time, dryRun bool, ignoreWarnings []*IgnoreWarnings) (deletedImages *DeletedImagesResponse) { + var manifests []*Manifest + + for _, d := range digests { + manifests = append(manifests, &Manifest{ + Repository: repository, + Digest: d, + }) + } + + post := &DeleteImagesRequest{ + DryRun: dryRun, + ActiveFrom: timeBefore, + Manifests: manifests, + IgnoreWarnings: ignoreWarnings, + } + + body, err := json.Marshal(post) + if err != nil { + panic(err) + } + + req, err := http.NewRequest("POST", "https://hub.docker.com/v2/namespaces/"+organization+"/delete-images", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "JWT "+client.token) + req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() + + rsp, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(rsp, &deletedImages) + if err != nil { + panic(string(rsp)) + } + + if resp.StatusCode != http.StatusOK { + panic(string(rsp)) + } + + return deletedImages } -func (client *client) GetTags(organization string, repository string, page int) TagsList { +func (client *client) GetImages(organization string, repository string, page int, timeBefore time.Time) (imageList *ImageList) { pageString := strconv.Itoa(page) + timeFrom := url.QueryEscape(timeBefore.Format(time.RFC3339)) - req, err := http.NewRequest("GET", "https://hub.docker.com/v2/repositories/"+organization+"/"+repository+"/tags/?page="+pageString, nil) + req, err := http.NewRequest("GET", "https://hub.docker.com/v2/namespaces/"+organization+"/repositories/"+repository+"/images?page="+pageString+"&page_size="+pageSize+"&ordering=last_activity&status=inactive&active_from="+timeFrom, nil) if err != nil { panic(err) } @@ -77,8 +117,11 @@ func (client *client) GetTags(organization string, repository string, page int) rsp, _ := ioutil.ReadAll(resp.Body) - var tagsList TagsList - json.Unmarshal(rsp, &tagsList) + if resp.StatusCode != http.StatusOK { + panic(string(rsp)) + } + + json.Unmarshal(rsp, &imageList) - return tagsList + return imageList } diff --git a/dockerhub/tag.go b/dockerhub/tag.go deleted file mode 100644 index c29315b..0000000 --- a/dockerhub/tag.go +++ /dev/null @@ -1,8 +0,0 @@ -package dockerhub - -import "time" - -type Tag struct { - Name string `json:"name"` - LastUpdated time.Time `json:"last_updated"` -} diff --git a/dockerhub/tags_list.go b/dockerhub/tags_list.go deleted file mode 100644 index 923bf52..0000000 --- a/dockerhub/tags_list.go +++ /dev/null @@ -1,7 +0,0 @@ -package dockerhub - -type TagsList struct { - Count int `json:"count"` - Next string `json:"next"` - Results []Tag `json:"results"` -} diff --git a/dockerhub/token.go b/dockerhub/token.go deleted file mode 100644 index a6ff050..0000000 --- a/dockerhub/token.go +++ /dev/null @@ -1,5 +0,0 @@ -package dockerhub - -type Token struct { - Token string `json:"token"` -} diff --git a/dockerhub/types.go b/dockerhub/types.go new file mode 100644 index 0000000..1e0b8df --- /dev/null +++ b/dockerhub/types.go @@ -0,0 +1,55 @@ +package dockerhub + +import "time" + +type Image struct { + Repository string `json:"repository"` + LastPushed time.Time `json:"last_pushed"` + LastPulled time.Time `json:"last_pulled"` + Digest string `json:"digest"` + Tags []*Tags `json:"tags"` +} + +type ImageList struct { + Count int `json:"count"` + Next string `json:"next"` + Results []Image `json:"results"` +} + +type DeleteImagesRequest struct { + DryRun bool `json:"dry_run"` + ActiveFrom time.Time `json:"active_from,omitempty"` + Manifests []*Manifest `json:"manifests"` + IgnoreWarnings []*IgnoreWarnings `json:"ignore_warnings"` +} + +type Manifest struct { + Repository string `json:"repository"` + Digest string `json:"digest"` +} +type IgnoreWarnings struct { + Repository string `json:"repository"` + Digest string `json:"digest"` + Warning string `json:"warning"` + Tags []string `json:"tags"` +} + +type Tags struct { + Tag string `json:"tag"` + IsCurrent bool `json:"is_current"` +} + +type DeletedImagesResponse struct { + Metrics *Metrics `json:"metrics"` +} + +type Metrics struct { + ManifestDeletes int `json:"manifest_deletes"` + ManifestErrors int `json:"manifest_errors"` + TagDeletes int `json:"tag_deletes"` + TagErrors int `json:"tag_errors"` +} + +type Token struct { + Token string `json:"token"` +} diff --git a/go.mod b/go.mod index 8d2c143..6b9fd28 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,5 @@ module dhlm -go 1.12 +go 1.16 -require ( - github.com/urfave/cli v1.20.0 - gopkg.in/urfave/cli.v1 v1.20.0 // indirect -) +require github.com/urfave/cli v1.20.0 diff --git a/go.sum b/go.sum index 17c13ec..03e08fc 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=