Skip to content

Commit

Permalink
refactor: remove cut off date logic, obselete if tags would use expir…
Browse files Browse the repository at this point in the history
…y label

Signed-off-by: KevFan <[email protected]>
  • Loading branch information
KevFan committed Sep 10, 2024
1 parent bb05f70 commit b6c621d
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 83 deletions.
50 changes: 18 additions & 32 deletions quay/quay_overflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"
"os"
"regexp"
"time"

"golang.org/x/exp/maps"
"oras.land/oras-go/pkg/registry/remote"
Expand All @@ -21,24 +20,19 @@ const (
)

var (
repo *string
baseURL *string
dryRun *bool
accessToken = os.Getenv("ACCESS_TOKEN")
preserveSubstrings = []string{
"latest",
// Preserve release branch images
"release-v*",
// Semver regex - vX.Y.Z(-rc1)
"^v(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)(?:-(?P<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
// Preserve semver release branch or semver tag regex - release-vX.Y.Z(-rc1) or vX.Y.Z(-rc1)
// Based on https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
"^(v|release-v)(?P<major>0|[1-9]\\d*)\\.(?P<minor>0|[1-9]\\d*)\\.(?P<patch>0|[1-9]\\d*)(?:-(?P<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
}
)

// Tag represents a tag in the repository.
type Tag struct {
Name string `json:"name"`
LastModified string `json:"last_modified"`
Expiration string `json:"expiration"`
Name string `json:"name"`
Expiration string `json:"expiration"`
}

// TagsResponse represents the structure of the API response that contains tags.
Expand All @@ -49,9 +43,9 @@ type TagsResponse struct {
}

func main() {
repo = flag.String("repo", "kuadrant/kuadrant-operator", "Repository name")
baseURL = flag.String("base-url", "https://quay.io/api/v1/repository/", "Base API URL")
dryRun = flag.Bool("dry-run", true, "Dry run")
repo := flag.String("repo", "kuadrant/kuadrant-operator", "Repository name")
baseURL := flag.String("base-url", "https://quay.io/api/v1/repository/", "Base API URL")
dryRun := flag.Bool("dry-run", true, "Dry run")
flag.Parse()

client := &http.Client{}
Expand All @@ -63,7 +57,7 @@ func main() {
}

// Fetch tags from the API
tags, err := fetchTags(client)
tags, err := fetchTags(client, baseURL, repo)
if err != nil {
logger.Fatalln("Error fetching tags:", err)
}
Expand All @@ -81,7 +75,7 @@ func main() {
if dryRun != nil && *dryRun {
logger.Printf("DRY RUN - Successfully deleted tag: %s\n", tagName)
} else {
if err := deleteTag(client, accessToken, tagName); err != nil {
if err := deleteTag(client, baseURL, repo, accessToken, tagName); err != nil {
logger.Println(err)
continue
}
Expand All @@ -99,13 +93,13 @@ func main() {

// fetchTags retrieves the tags from the repository using the Quay.io API.
// https://docs.quay.io/api/swagger/#!/tag/listRepoTags
func fetchTags(client remote.Client) ([]Tag, error) {
func fetchTags(client remote.Client, baseURL, repo *string) ([]Tag, error) {
allTags := make([]Tag, 0)

i := 1

for {
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s/tag/?page=%d&limit=%d", *baseURL, *repo, i, pageLimit), nil)
req, err := http.NewRequest("GET", fmt.Sprintf("%v%v/tag/?page=%d&limit=%d", baseURL, repo, i, pageLimit), nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
Expand Down Expand Up @@ -155,8 +149,8 @@ func fetchTags(client remote.Client) ([]Tag, error) {
// deleteTag sends a DELETE request to remove the specified tag from the repository
// Returns nil if successful, error otherwise
// https://docs.quay.io/api/swagger/#!/tag/deleteFullTag
func deleteTag(client remote.Client, accessToken, tagName string) error {
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s%s/tag/%s", *baseURL, *repo, tagName), nil)
func deleteTag(client remote.Client, baseURL, repo *string, accessToken, tagName string) error {
req, err := http.NewRequest("DELETE", fmt.Sprintf("%v%v/tag/%s", baseURL, repo, tagName), nil)
if err != nil {
return fmt.Errorf("error creating DELETE request: %s", err)

Check failure on line 155 in quay/quay_overflow.go

View workflow job for this annotation

GitHub Actions / Lint

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
Expand All @@ -178,11 +172,8 @@ func deleteTag(client remote.Client, accessToken, tagName string) error {

// filterTags takes a slice of tags and preserves string regex and returns two maps: one for tags to delete and one for remaining tags.
func filterTags(tags []Tag, preserveSubstrings []string) (map[string]struct{}, map[string]struct{}, error) {
// Calculate the cutoff time
cutOffTime := time.Now().AddDate(0, 0, 0).Add(0 * time.Hour).Add(-1 * time.Minute)

tagsToDelete := make(map[string]struct{})
perservedTags := make(map[string]struct{})
preservedTags := make(map[string]struct{})

// Compile the regexes for each preserve substring
var preserveRegexes []*regexp.Regexp

Check failure on line 179 in quay/quay_overflow.go

View workflow job for this annotation

GitHub Actions / Lint

Consider pre-allocating `preserveRegexes` (prealloc)
Expand Down Expand Up @@ -211,17 +202,12 @@ func filterTags(tags []Tag, preserveSubstrings []string) (map[string]struct{}, m
}
}

lastModified, err := time.Parse(time.RFC1123, tag.LastModified)
if err != nil {
return nil, nil, err
}

if lastModified.Before(cutOffTime) && !preserve {
if !preserve {
tagsToDelete[tag.Name] = struct{}{}
} else {
perservedTags[tag.Name] = struct{}{}
preservedTags[tag.Name] = struct{}{}
}
}

return tagsToDelete, perservedTags, nil
return tagsToDelete, preservedTags, nil
}
92 changes: 41 additions & 51 deletions quay/quay_overflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"errors"
"io"
"net/http"
"slices"
"strings"
"testing"
"time"

"k8s.io/client-go/rest"
"oras.land/oras-go/pkg/registry/remote"
)

var _ remote.Client = &MockHTTPClient{}

type MockHTTPClient struct {
wantErr bool
mutateFn func(res *http.Response)
Expand All @@ -30,11 +33,14 @@ func (m MockHTTPClient) Do(_ *http.Request) (*http.Response, error) {
return resp, nil
}

var _ rest.HTTPClient = &MockHTTPClient{}
var (
testBaseUrl = "https://quay.io/api/v1/"

Check failure on line 37 in quay/quay_overflow_test.go

View workflow job for this annotation

GitHub Actions / Lint

var-naming: var testBaseUrl should be testBaseURL (revive)
testRepo = "testOrg/kuadrant-operator"
)

func Test_fetchTags(t *testing.T) {
t.Run("test error making request", func(t *testing.T) {
tags, err := fetchTags(&MockHTTPClient{wantErr: true})
tags, err := fetchTags(&MockHTTPClient{wantErr: true}, &testBaseUrl, &testRepo)

if err == nil {
t.Error("error expected")
Expand All @@ -53,7 +59,7 @@ func Test_fetchTags(t *testing.T) {
tags, err := fetchTags(&MockHTTPClient{mutateFn: func(res *http.Response) {
res.Status = string(rune(400))
res.Body = io.NopCloser(bytes.NewReader(nil))
}})
}}, &testBaseUrl, &testRepo)

if err == nil {
t.Error("error expected")
Expand All @@ -72,7 +78,7 @@ func Test_fetchTags(t *testing.T) {
tags, err := fetchTags(&MockHTTPClient{mutateFn: func(res *http.Response) {
res.Status = string(rune(200))
res.Body = io.NopCloser(bytes.NewReader([]byte("{notTags: error}")))
}})
}}, &testBaseUrl, &testRepo)

if err == nil {
t.Error("error expected")
Expand All @@ -90,16 +96,16 @@ func Test_fetchTags(t *testing.T) {
t.Run("test successful response with tags", func(t *testing.T) {
mockJSONResponse := `{
"tags": [
{"name": "v1.0.0", "last_modified": "Mon, 02 Jan 2006 15:04:05 MST"},
{"name": "v1.1.0", "last_modified": "Tue, 03 Jan 2006 15:04:05 MST"},
{"name": "latest", "last_modified": "Wed, 04 Jan 2006 15:04:05 MST"}
{"name": "v1.0.0"},
{"name": "v1.1.0"},
{"name": "latest"}
]
}`

tags, err := fetchTags(&MockHTTPClient{mutateFn: func(res *http.Response) {
res.StatusCode = http.StatusOK
res.Body = io.NopCloser(bytes.NewReader([]byte(mockJSONResponse)))
}})
}}, &testBaseUrl, &testRepo)

if err != nil {
t.Fatalf("unexpected error: %v", err)
Expand All @@ -110,15 +116,15 @@ func Test_fetchTags(t *testing.T) {
t.Fatalf("expected 3 tags, got %d", len(tags))
}

expectedTags := map[string]string{
"v1.0.0": "Mon, 02 Jan 2006 15:04:05 MST",
"v1.1.0": "Tue, 03 Jan 2006 15:04:05 MST",
"latest": "Wed, 04 Jan 2006 15:04:05 MST",
expectedTags := []string{
"v1.0.0",
"v1.1.0",
"latest",
}

for _, tag := range tags {
if expectedDate, ok := expectedTags[tag.Name]; !ok || expectedDate != tag.LastModified {
t.Errorf("unexpected tag: got %v, expected %v", tag, expectedTags[tag.Name])
if !slices.Contains(expectedTags, tag.Name) {
t.Errorf("unexpected tag: %v, does not exist in expected tags %v", tag, expectedTags)
}
}
})
Expand All @@ -131,7 +137,7 @@ func Test_deleteTag(t *testing.T) {
res.Body = io.NopCloser(bytes.NewReader(nil))
}}

err := deleteTag(client, "fake_access_token", "v1.0.0")
err := deleteTag(client, &testBaseUrl, &testRepo, "fake_access_token", "v1.0.0")

if err != nil {
t.Error("expected successful delete, got error")
Expand All @@ -144,7 +150,7 @@ func Test_deleteTag(t *testing.T) {
res.Body = io.NopCloser(bytes.NewReader([]byte("internal server error")))
}}

err := deleteTag(client, "fake_access_token", "v1.0.0")
err := deleteTag(client, &testBaseUrl, &testRepo, "fake_access_token", "v1.0.0")

if err == nil {
t.Error("expected failure, got success")
Expand All @@ -154,7 +160,7 @@ func Test_deleteTag(t *testing.T) {
t.Run("test error making delete request", func(t *testing.T) {
client := &MockHTTPClient{wantErr: true}

err := deleteTag(client, "fake_access_token", "v1.0.0")
err := deleteTag(client, &testBaseUrl, &testRepo, "fake_access_token", "v1.0.0")

if err == nil {
t.Error("expected failure, got success")
Expand All @@ -165,13 +171,13 @@ func Test_deleteTag(t *testing.T) {
func Test_filterTags(t *testing.T) {
t.Run("test filter tags correctly", func(t *testing.T) {
tags := []Tag{
{Name: "nightly-build", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, should be deleted
{Name: "v1.1.0", LastModified: time.Now().Format(time.RFC1123)}, // Recent tag, should be kept
{Name: "latest", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, but name contains preserveSubstring latest
{Name: "release-v1.2.3", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, but name contains preserveSubstring release-v*
{Name: "v1.0.0", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, but name contains preserveSubstring semver release
{Name: "v1.2.0-rc1", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, but name contains preserveSubstring semver release-candidate
{Name: "expiry_set", LastModified: time.Now().Add(-24 * time.Hour).Format(time.RFC1123), Expiration: time.Now().Format(time.RFC1123)}, // Old tag, but already has an expiry set
{Name: "nightly-build"}, // Not a preserved tag, should be deleted
{Name: "latest"}, // Preserved tag, name is latest
{Name: "release-v1.0.0"}, // Preserved tag, name contains preserveSubstring branch release semver, release-v*
{Name: "v1.0.0"}, // Preserved tag, but name contains preserveSubstring tag semver release
{Name: "v1.1.0-rc1"}, // Preserved tag, but name contains preserveSubstring tag semver release-candidate
{Name: "expiry_set", Expiration: time.Now().Format(time.RFC1123)}, // Skipped tag, already has an expiry set
{Name: "release-not-semver"}, // Not a preserved tag, should be deleted
}

tagsToDelete, remainingTags, err := filterTags(tags, preserveSubstrings)
Expand All @@ -180,43 +186,39 @@ func Test_filterTags(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}

if len(tagsToDelete) != 1 || len(remainingTags) != 6 {
t.Fatalf("expected 1 tag to delete and 6 remaining, got %d to delete and %d remaining", len(tagsToDelete), len(remainingTags))
if len(tagsToDelete) != 2 || len(remainingTags) != 4 {
t.Fatalf("expected 2 tag to delete and 4 remaining, got %d to delete and %d remaining", len(tagsToDelete), len(remainingTags))
}

if _, ok := tagsToDelete["nightly-build"]; !ok {
t.Error("expected nightly-build to be deleted")
}

if _, ok := remainingTags["v1.1.0"]; !ok {
t.Error("expected v1.1.0 to be kept")
if _, ok := tagsToDelete["release-not-semver"]; !ok {
t.Error("expected nightly-build to be deleted")
}

if _, ok := remainingTags["latest"]; !ok {
t.Error("expected latest to be kept")
}

if _, ok := remainingTags["release-v1.2.3"]; !ok {
t.Error("expected release-v1.2.3 to be kept")
if _, ok := remainingTags["release-v1.0.0"]; !ok {
t.Error("expected release-v1.0.0 to be kept")
}

if _, ok := remainingTags["v1.0.0"]; !ok {
t.Error("expected v1.0.0 to be kept")
}

if _, ok := remainingTags["v1.2.0-rc1"]; !ok {
t.Error("expected v1.2.0-rc1 to be kept")
}

if _, ok := remainingTags["expiry_set"]; !ok {
t.Error("expected expiry_set to be kept")
if _, ok := remainingTags["v1.1.0-rc1"]; !ok {
t.Error("expected v1.1.0-rc1 to be kept")
}
})

t.Run("test filter tags with no deletions", func(t *testing.T) {
tags := []Tag{
{Name: "v1.1.0", LastModified: time.Now().Format(time.RFC1123)}, // Preserved tag, should be kept
{Name: "recent", LastModified: time.Now().Format(time.RFC1123)}, // Recent tag, should be kept
{Name: "v1.1.0"}, // Preserved tag, should be kept
{Name: "latest"}, // Preserved tag, should be kept
}

tagsToDelete, remainingTags, err := filterTags(tags, preserveSubstrings)
Expand All @@ -229,16 +231,4 @@ func Test_filterTags(t *testing.T) {
t.Fatalf("expected 0 tags to delete and 2 remaining, got %d to delete and %d remaining", len(tagsToDelete), len(remainingTags))
}
})

t.Run("test error unexpected time format", func(t *testing.T) {
tags := []Tag{
{Name: "v1.1.0", LastModified: time.Now().Format(time.ANSIC)},
}

_, _, err := filterTags(tags, preserveSubstrings)

if err == nil {
t.Fatal("expected error, got success")
}
})
}

0 comments on commit b6c621d

Please sign in to comment.