From e6d498064c3ca85f2af97fbdfd74422c8657b7fe Mon Sep 17 00:00:00 2001 From: dlaw4608 Date: Thu, 29 Aug 2024 15:19:04 +0100 Subject: [PATCH] Includes quay_overflow.go file and quay_overflow_test.go Signed-off-by: dlaw4608 --- quay/quay_overflow.go | 14 ++- quay/quay_overflow_test.go | 208 +++++++++++++++++++++++++++++++++++++ quay/test/mirror_repo.sh | 19 ---- 3 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 quay/quay_overflow_test.go delete mode 100755 quay/test/mirror_repo.sh diff --git a/quay/quay_overflow.go b/quay/quay_overflow.go index e7880db20..8a2d9d8ab 100644 --- a/quay/quay_overflow.go +++ b/quay/quay_overflow.go @@ -9,10 +9,12 @@ import ( "os" "strings" "time" + + "k8s.io/client-go/rest" ) const ( - repo = "test_org_123/kuadrant-operator" + repo = "kuadrant/kuadrant-operator" baseURL = "https://quay.io/api/v1/repository/" ) @@ -20,7 +22,7 @@ var ( robotPass = os.Getenv("ROBOT_PASS") robotUser = os.Getenv("ROBOT_USER") accessToken = os.Getenv("ACCESS_TOKEN") - preserveSubstring = "danlaw345" // Example Tag name that wont be deleted i.e relevant tags + preserveSubstring = "latest" // Example Tag name that wont be deleted i.e relevant tags ) // Tag represents a tag in the repository. @@ -62,7 +64,9 @@ func main() { } // fetchTags retrieves the tags from the repository using the Quay.io API. -func fetchTags(client *http.Client) ([]Tag, error) { +func fetchTags(client rest.HTTPClient) ([]Tag, error) { + // TODO - DO you want to seperate out builidng the request to a function to unit test? + // TODO - Is adding the headers even needed to fetch tags for a public repo? req, err := http.NewRequest("GET", baseURL+repo+"/tag", nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) @@ -138,7 +142,7 @@ func containsSubstring(tagName, substring string) bool { // deleteTag sends a DELETE request to remove the specified tag from the repository // Returns true if successful, false otherwise -func deleteTag(client *http.Client, accessToken, tagName string) bool { +func deleteTag(client rest.HTTPClient, accessToken, tagName string) bool { req, err := http.NewRequest("DELETE", baseURL+repo+"/tag/"+tagName, nil) if err != nil { fmt.Println("Error creating DELETE request:", err) @@ -161,4 +165,4 @@ func deleteTag(client *http.Client, accessToken, tagName string) bool { fmt.Printf("Failed to delete tag %s: Status code %d\nBody: %s\n", tagName, resp.StatusCode, string(body)) return false } -} +} \ No newline at end of file diff --git a/quay/quay_overflow_test.go b/quay/quay_overflow_test.go new file mode 100644 index 000000000..0def55de1 --- /dev/null +++ b/quay/quay_overflow_test.go @@ -0,0 +1,208 @@ +package main + +import ( + "bytes" + "errors" + "io" + "net/http" + "strings" + "testing" + "time" + + "k8s.io/client-go/rest" +) + +type MockHTTPClient struct { + wantErr bool + mutateFn func(res *http.Response) +} + +func (m MockHTTPClient) Do(request *http.Request) (*http.Response, error) { + if m.wantErr { + return nil, errors.New("oops") + } + + resp := &http.Response{} + if m.mutateFn != nil { + m.mutateFn(resp) + } + + return resp, nil +} + +var _ rest.HTTPClient = &MockHTTPClient{} + +func Test_fetchTags(t *testing.T) { + t.Run("test error making request", func(t *testing.T) { + tags, err := fetchTags(&MockHTTPClient{wantErr: true}) + + if err == nil { + t.Error("error expected") + } + + if err.Error() != "error making request: oops" { + t.Errorf("error expected, got %s", err.Error()) + } + + if tags != nil { + t.Error("expected nil tags") + } + }) + + t.Run("test error for non-200 status codes", func(t *testing.T) { + tags, err := fetchTags(&MockHTTPClient{mutateFn: func(res *http.Response) { + res.Status = string(rune(400)) + res.Body = io.NopCloser(bytes.NewReader(nil)) + }}) + + if err == nil { + t.Error("error expected") + } + + if strings.Contains(err.Error(), "tags, error: received status code 400") { + t.Errorf("error expected, got %s", err.Error()) + } + + if tags != nil { + t.Error("expected nil tags") + } + }) + + t.Run("test error parsing json", func(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}"))) + }}) + + if err == nil { + t.Error("error expected") + } + + if strings.Contains(err.Error(), "error unmarshalling response:") { + t.Errorf("error expected, got %s", err.Error()) + } + + if tags != nil { + t.Error("expected nil tags") + } + }) + + 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"} + ] + }` + + tags, err := fetchTags(&MockHTTPClient{mutateFn: func(res *http.Response) { + res.StatusCode = http.StatusOK + res.Body = io.NopCloser(bytes.NewReader([]byte(mockJSONResponse))) + }}) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Validate the returned tags + if len(tags) != 3 { + 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", + } + + 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]) + } + } + }) +} + +func Test_deleteTag(t *testing.T) { + t.Run("test successful delete", func(t *testing.T) { + client := &MockHTTPClient{mutateFn: func(res *http.Response) { + res.StatusCode = http.StatusNoContent + res.Body = io.NopCloser(bytes.NewReader(nil)) + }} + + success := deleteTag(client, "fake_access_token", "v1.0.0") + + if !success { + t.Error("expected successful delete, got failure") + } + }) + + t.Run("test delete with error response", func(t *testing.T) { + client := &MockHTTPClient{mutateFn: func(res *http.Response) { + res.StatusCode = http.StatusInternalServerError + res.Body = io.NopCloser(bytes.NewReader([]byte("internal server error"))) + }} + + success := deleteTag(client, "fake_access_token", "v1.0.0") + + if success { + t.Error("expected failure, got success") + } + }) + + t.Run("test error making delete request", func(t *testing.T) { + client := &MockHTTPClient{wantErr: true} + + success := deleteTag(client, "fake_access_token", "v1.0.0") + + if success { + t.Error("expected failure, got success") + } + }) +} + +func Test_filterTags(t *testing.T) { + t.Run("test filter tags correctly", func(t *testing.T) { + tags := []Tag{ + {"nightly-build", time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, should be deleted + {"v1.1.0", time.Now().Format(time.RFC1123)}, // Recent tag, should be kept + {"latest", time.Now().Add(-24 * time.Hour).Format(time.RFC1123)}, // Old tag, but name contains preserveSubstring + } + + preserveSubstring := "latest" + + tagsToDelete, remainingTags := filterTags(tags, preserveSubstring) + + if len(tagsToDelete) != 1 || len(remainingTags) != 2 { + t.Fatalf("expected 1 tag to delete and 2 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 := remainingTags["latest"]; !ok { + t.Error("expected latest to be kept") + } + }) + + t.Run("test filter tags with no deletions", func(t *testing.T) { + tags := []Tag{ + {"v1.1.0", time.Now().Format(time.RFC1123)}, // Recent tag, should be kept + {"latest", time.Now().Format(time.RFC1123)}, // Recent tag, should be kept + } + + preserveSubstring := "latest" + + tagsToDelete, remainingTags := filterTags(tags, preserveSubstring) + + if len(tagsToDelete) != 0 || len(remainingTags) != 2 { + t.Fatalf("expected 0 tags to delete and 2 remaining, got %d to delete and %d remaining", len(tagsToDelete), len(remainingTags)) + } + }) +} diff --git a/quay/test/mirror_repo.sh b/quay/test/mirror_repo.sh deleted file mode 100755 index 5817a99a8..000000000 --- a/quay/test/mirror_repo.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Old and new repository info -old_repo="quay.io/dlawton/kuadrant-operator" -new_repo="quay.io/test_org_123/kuadrant-operator" - -# Get list of tags from the old repository -tags=$(curl -s -u $ROBOT_USER:$ROBOT_PASS "https://quay.io/api/v1/repository/dlawton/kuadrant-operator/tag/?onlyActiveTags=true" | jq -r '.tags[].name') - -# Loop through each tag and migrate it -for tag in $tags; do - echo "Migrating tag: $tag" - - docker pull $old_repo:$tag - docker tag $old_repo:$tag $new_repo:$tag - docker push $new_repo:$tag - - echo "Successfully migrated tag: $tag" -done