1
+ package main
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "net/http"
9
+ "os"
10
+ "strings"
11
+ "time"
12
+
13
+ "k8s.io/client-go/rest"
14
+ )
15
+
16
+ const (
17
+ repo = "kuadrant/kuadrant-operator"
18
+ baseURL = "https://quay.io/api/v1/repository/"
19
+ )
20
+
21
+ var (
22
+ robotPass = os .Getenv ("ROBOT_PASS" )
23
+ robotUser = os .Getenv ("ROBOT_USER" )
24
+ accessToken = os .Getenv ("ACCESS_TOKEN" )
25
+ preserveSubstring = "latest" // Example Tag name that wont be deleted i.e relevant tags
26
+ )
27
+
28
+ // Tag represents a tag in the repository.
29
+ type Tag struct {
30
+ Name string `json:"name"`
31
+ LastModified string `json:"last_modified"`
32
+ }
33
+
34
+ // TagsResponse represents the structure of the API response that contains tags.
35
+ type TagsResponse struct {
36
+ Tags []Tag `json:"tags"`
37
+ }
38
+
39
+ func main () {
40
+ client := & http.Client {}
41
+
42
+ // Fetch tags from the API
43
+ tags , err := fetchTags (client )
44
+ if err != nil {
45
+ fmt .Println ("Error fetching tags:" , err )
46
+ return
47
+ }
48
+
49
+ // Use filterTags to get tags to delete and remaining tags
50
+ tagsToDelete , remainingTags := filterTags (tags , preserveSubstring )
51
+
52
+ // Delete tags and update remainingTags
53
+ for tagName := range tagsToDelete {
54
+ if deleteTag (client , accessToken , tagName ) {
55
+ delete (remainingTags , tagName ) // Remove deleted tag from remainingTags
56
+ }
57
+ }
58
+
59
+ // Print remaining tags
60
+ fmt .Println ("Remaining tags:" )
61
+ for tag := range remainingTags {
62
+ fmt .Println (tag )
63
+ }
64
+ }
65
+
66
+ // fetchTags retrieves the tags from the repository using the Quay.io API.
67
+ func fetchTags (client rest.HTTPClient ) ([]Tag , error ) {
68
+ // TODO - DO you want to seperate out builidng the request to a function to unit test?
69
+ // TODO - Is adding the headers even needed to fetch tags for a public repo?
70
+ req , err := http .NewRequest ("GET" , baseURL + repo + "/tag" , nil )
71
+ if err != nil {
72
+ return nil , fmt .Errorf ("error creating request: %w" , err )
73
+ }
74
+
75
+ // Prioritize Bearer token for authorization
76
+ if accessToken != "" {
77
+ req .Header .Add ("Authorization" , "Bearer " + accessToken )
78
+ } else {
79
+ // Fallback to Basic Authentication if no access token
80
+ auth := base64 .StdEncoding .EncodeToString ([]byte (robotUser + ":" + robotPass ))
81
+ req .Header .Add ("Authorization" , "Basic " + auth )
82
+ }
83
+
84
+ // Execute the request
85
+ resp , err := client .Do (req )
86
+ if err != nil {
87
+ return nil , fmt .Errorf ("error making request: %w" , err )
88
+ }
89
+ defer resp .Body .Close ()
90
+
91
+ // Handle possible non-200 status codes
92
+ if resp .StatusCode != http .StatusOK {
93
+ body , _ := io .ReadAll (resp .Body )
94
+ return nil , fmt .Errorf ("error: received status code %d\n Body: %s" , resp .StatusCode , string (body ))
95
+ }
96
+
97
+ // Read the response body
98
+ body , err := io .ReadAll (resp .Body )
99
+ if err != nil {
100
+ return nil , fmt .Errorf ("error reading response body: %w" , err )
101
+ }
102
+
103
+ // Parse the JSON response
104
+ var tagsResp TagsResponse
105
+ if err := json .Unmarshal (body , & tagsResp ); err != nil {
106
+ return nil , fmt .Errorf ("error unmarshalling response: %w" , err )
107
+ }
108
+
109
+ return tagsResp .Tags , nil
110
+ }
111
+
112
+ // filterTags takes a slice of tags and returns two maps: one for tags to delete and one for remaining tags.
113
+ func filterTags (tags []Tag , preserveSubstring string ) (map [string ]struct {}, map [string ]struct {}) {
114
+ // Calculate the cutoff time
115
+ cutOffTime := time .Now ().AddDate (0 , 0 , 0 ).Add (0 * time .Hour ).Add (- 1 * time .Minute )
116
+
117
+ tagsToDelete := make (map [string ]struct {})
118
+ remainingTags := make (map [string ]struct {})
119
+
120
+ for _ , tag := range tags {
121
+ // Parse the LastModified timestamp
122
+ lastModified , err := time .Parse (time .RFC1123 , tag .LastModified )
123
+ if err != nil {
124
+ fmt .Println ("Error parsing time:" , err )
125
+ continue
126
+ }
127
+
128
+ // Check if tag should be deleted
129
+ if lastModified .Before (cutOffTime ) && ! containsSubstring (tag .Name , preserveSubstring ) {
130
+ tagsToDelete [tag .Name ] = struct {}{}
131
+ } else {
132
+ remainingTags [tag .Name ] = struct {}{}
133
+ }
134
+ }
135
+
136
+ return tagsToDelete , remainingTags
137
+ }
138
+
139
+ func containsSubstring (tagName , substring string ) bool {
140
+ return strings .Contains (tagName , substring )
141
+ }
142
+
143
+ // deleteTag sends a DELETE request to remove the specified tag from the repository
144
+ // Returns true if successful, false otherwise
145
+ func deleteTag (client rest.HTTPClient , accessToken , tagName string ) bool {
146
+ req , err := http .NewRequest ("DELETE" , baseURL + repo + "/tag/" + tagName , nil )
147
+ if err != nil {
148
+ fmt .Println ("Error creating DELETE request:" , err )
149
+ return false
150
+ }
151
+ req .Header .Add ("Authorization" , "Bearer " + accessToken )
152
+
153
+ resp , err := client .Do (req )
154
+ if err != nil {
155
+ fmt .Println ("Error deleting tag:" , err )
156
+ return false
157
+ }
158
+ defer resp .Body .Close ()
159
+
160
+ if resp .StatusCode == http .StatusNoContent {
161
+ fmt .Printf ("Successfully deleted tag: %s\n " , tagName )
162
+ return true
163
+ } else {
164
+ body , _ := io .ReadAll (resp .Body )
165
+ fmt .Printf ("Failed to delete tag %s: Status code %d\n Body: %s\n " , tagName , resp .StatusCode , string (body ))
166
+ return false
167
+ }
168
+ }
0 commit comments