-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathimage_functions.go
More file actions
289 lines (264 loc) · 10.8 KB
/
Copy pathimage_functions.go
File metadata and controls
289 lines (264 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package common
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/Azure/acr-cli/acr"
"github.com/Azure/acr-cli/acr/acrapi"
"github.com/Azure/acr-cli/internal/api"
"github.com/Azure/acr-cli/internal/container/set"
"github.com/dlclark/regexp2"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)
const (
headerLink = "Link"
mediaTypeDockerManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
defaultRegexpOptions regexp2.RegexOptions = regexp2.RE2 // This option will turn on compatibility mode so that it uses the group rules in regexp
defaultRegexpMatchTimeoutSeconds int64 = 60
mediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"
)
func GetAllRepositoryNames(ctx context.Context, client acrapi.BaseClientAPI, pageSize int32) ([]string, error) {
allRepoNames := make([]string, 0)
lastName := ""
for {
repos, err := client.GetRepositories(ctx, lastName, &pageSize)
if err != nil {
return nil, err
}
if repos.Names == nil || len(*repos.Names) == 0 {
break
}
allRepoNames = append(allRepoNames, *repos.Names...)
lastName = allRepoNames[len(allRepoNames)-1]
}
return allRepoNames, nil
}
// GetMatchingRepos get all repositories in current registry, that match the provided regular expression
func GetMatchingRepos(repoNames []string, repoRegex string, regexMatchTimeout int64) ([]string, error) {
filter, err := BuildRegexFilter(repoRegex, regexMatchTimeout)
if err != nil {
return nil, err
}
var matchedRepos []string
for _, repo := range repoNames {
matched, err := filter.MatchString(repo)
if err != nil {
// The only error regexp2 can throw is a timeout error
return nil, err
}
if matched {
matchedRepos = append(matchedRepos, repo)
}
}
return matchedRepos, nil
}
// GetRepositoryAndTagRegex splits the strings that are in the form <repository>:<regex filter>
func GetRepositoryAndTagRegex(filter string) (string, string, error) {
// This only selects colons that are not apart of a non-capture group
// Note: regexp2 doesn't have .Split support yet, so we just replace the colon with another delimitter \r\n
// We choose \r\n since it is an escape sequence that cannot be a part of repo name or a tag
// For information on how this expression was written, see https://regexr.com/6jqp3
noncaptureGroupSupport := regexp2.MustCompile(`(?<!\(\?[imsU-]{0,5}|\[*\^*\[\^*):(?!\]\]*)`, defaultRegexpOptions)
// Note: We could just find the first 1, however we want to know if there are more than 1 colon that is not part of a non-capture group
newlineDelimitted, err := noncaptureGroupSupport.Replace(filter, "\r\n", -1, -1)
if err != nil {
return "", "", errors.New("could not replace split filter by repo and tag")
}
repoAndRegex := strings.Split(newlineDelimitted, "\r\n")
if len(repoAndRegex) != 2 {
return "", "", errors.New("unable to correctly parse filter flag")
}
if repoAndRegex[0] == "" {
return "", "", errors.New("missing repository name/expression")
}
if repoAndRegex[1] == "" {
return "", "", errors.New("missing tag name/expression")
}
return repoAndRegex[0], repoAndRegex[1], nil
}
// CollectTagFilters collects all matching repos and collects the associated tag filters
func CollectTagFilters(ctx context.Context, rawFilters []string, client acrapi.BaseClientAPI, regexMatchTimeout int64, repoPageSize int32) (map[string]string, error) {
allRepoNames, err := GetAllRepositoryNames(ctx, client, repoPageSize)
if err != nil {
return nil, err
}
tagFilters := map[string]string{}
for _, filter := range rawFilters {
repoRegex, tagRegex, err := GetRepositoryAndTagRegex(filter)
if err != nil {
return nil, err
}
repoNames, err := GetMatchingRepos(allRepoNames, "^"+repoRegex+"$", regexMatchTimeout)
if err != nil {
return nil, err
}
for _, repoName := range repoNames {
if _, ok := tagFilters[repoName]; ok {
// To only iterate through a repo once a big regex filter is made of all the filters of a particular repo.
tagFilters[repoName] = tagFilters[repoName] + "|" + tagRegex
} else {
tagFilters[repoName] = tagRegex
}
}
}
return tagFilters, nil
}
func GetLastTagFromResponse(resultTags *acr.RepositoryTagsType) string {
// The lastTag is updated to keep the for loop going.
if resultTags.Header == nil {
return ""
}
link := resultTags.Header.Get(headerLink)
if len(link) == 0 {
return ""
}
queryString := strings.Split(link, "?")
if len(queryString) <= 1 {
return ""
}
queryStringToParse := strings.Split(queryString[1], ">")
vals, err := url.ParseQuery(queryStringToParse[0])
if err != nil {
return ""
}
return vals.Get("last")
}
// GetUntaggedManifests gets all the manifests for the command to be executed on. The command will be executed on this manifest if it does not
// have any tag and does not form part of a manifest list that has tags referencing it. If the purge command is to be executed,
// the manifest should also not have a tag and not have a subject manifest.
func GetUntaggedManifests(ctx context.Context, acrClient api.AcrCLIClientInterface, loginURL string, repoName string, dryRun bool, ignoreReferrerManifests bool, UntagLimit int) (*[]string, error) {
lastManifestDigest := ""
var manifestsForCommand []string
resultManifests, err := acrClient.GetAcrManifests(ctx, repoName, "", lastManifestDigest)
if err != nil {
if resultManifests != nil && resultManifests.Response.Response != nil && resultManifests.StatusCode == http.StatusNotFound {
fmt.Printf("%s repository not found\n", repoName)
return &manifestsForCommand, nil
}
return nil, err
}
// This will act as a set. If a key is present, then the command shouldn't be executed because it is referenced by a multiarch manifest
// or the manifest has subjects attached
ignoreList := set.New[string]()
var candidates []acr.ManifestAttributesBase
for resultManifests != nil && resultManifests.ManifestsAttributes != nil {
manifests := *resultManifests.ManifestsAttributes
for _, manifest := range manifests {
if manifest.Tags != nil {
// If a manifest has Tags and its media type supports multiarch manifest, we will
// iterate all its dependent manifests and mark them to not have the command execute on them.
if err = AddDependentManifestsToIgnoreList(ctx, manifest, ignoreList, acrClient, repoName); err != nil {
return nil, err
}
} else {
if ignoreReferrerManifests {
// If a manifest does not have Tags and its media type supports subject, we will
// check if the subject exists. If so, the manifest is marked not to be affected by the command.
if candidates, err = UpdateForManifestWithoutSubjectToDelete(ctx, manifest, ignoreList, candidates, acrClient, repoName); err != nil {
return nil, err
}
} else {
if *manifest.MediaType != v1.MediaTypeImageManifest {
candidates = append(candidates, manifest)
}
}
}
}
// Get the last manifest digest from the last manifest from manifests.
lastManifestDigest = *manifests[len(manifests)-1].Digest
// Use this new digest to find next batch of manifests.
resultManifests, err = acrClient.GetAcrManifests(ctx, repoName, "", lastManifestDigest)
if err != nil {
return nil, err
}
if UntagLimit > 0 && len(candidates) >= UntagLimit {
// If the number of candidates is greater than the limit, we stop fetching more manifests.
break
}
}
// Remove all manifests that should not be deleted
for i := 0; i < len(candidates); i++ {
if !ignoreList.Contains(*candidates[i].Digest) {
// if a manifest has no tags, is not part of a manifest list and can be deleted then it is added to the
// manifestsForCommand array.
if *(candidates[i].ChangeableAttributes).DeleteEnabled && *(candidates[i].ChangeableAttributes).WriteEnabled {
manifestsForCommand = append(manifestsForCommand, *candidates[i].Digest)
if dryRun && !ignoreReferrerManifests {
fmt.Printf("%s/%s@%s\n", loginURL, repoName, *candidates[i].Digest)
}
}
}
}
return &manifestsForCommand, nil
}
// AddDependentManifestsToIgnoreList adds the dependant manifest to doNotAffect if the referred manifest has tags.
func AddDependentManifestsToIgnoreList(ctx context.Context, manifest acr.ManifestAttributesBase, doNotAffect set.Set[string], acrClient api.AcrCLIClientInterface, repoName string) error {
switch *manifest.MediaType {
case mediaTypeDockerManifestList, v1.MediaTypeImageIndex:
var manifestBytes []byte
manifestBytes, err := acrClient.GetManifest(ctx, repoName, *manifest.Digest)
if err != nil {
return err
}
// this struct defines a customized struct for manifests
// which is used to parse the content of a multiarch manifest
mam := struct {
Manifests []v1.Descriptor `json:"manifests"`
}{}
if err = json.Unmarshal(manifestBytes, &mam); err != nil {
return err
}
for _, dependentManifest := range mam.Manifests {
doNotAffect.Add(dependentManifest.Digest.String())
}
}
return nil
}
// UpdateForManifestWithoutSubjectToDelete adds the manifest to candidatesToDelete
// if the manifest does not have subject, otherwise add it to doNotDelete.
func UpdateForManifestWithoutSubjectToDelete(ctx context.Context, manifest acr.ManifestAttributesBase, doNotDelete set.Set[string], candidatesToDelete []acr.ManifestAttributesBase, acrClient api.AcrCLIClientInterface, repoName string) ([]acr.ManifestAttributesBase, error) {
switch *manifest.MediaType {
case mediaTypeArtifactManifest, v1.MediaTypeImageManifest, v1.MediaTypeImageIndex:
var manifestBytes []byte
manifestBytes, err := acrClient.GetManifest(ctx, repoName, *manifest.Digest)
if err != nil {
return nil, err
}
// this struct defines a customized struct for manifests which
// is used to parse the content of a manifest references a subject
mws := struct {
Subject *v1.Descriptor `json:"subject,omitempty"`
}{}
if err = json.Unmarshal(manifestBytes, &mws); err != nil {
return nil, err
}
if mws.Subject != nil {
doNotDelete.Add(*manifest.Digest)
} else {
candidatesToDelete = append(candidatesToDelete, manifest)
}
default:
candidatesToDelete = append(candidatesToDelete, manifest)
}
return candidatesToDelete, nil
}
// BuildRegexFilter compiles a regex state machine from a regex expression
func BuildRegexFilter(expression string, regexpMatchTimeoutSeconds int64) (*regexp2.Regexp, error) {
regexp, err := regexp2.Compile(expression, defaultRegexpOptions)
if err != nil {
return nil, err
}
// A timeout value must always be set
if regexpMatchTimeoutSeconds <= 0 {
regexpMatchTimeoutSeconds = defaultRegexpMatchTimeoutSeconds
}
regexp.MatchTimeout = time.Duration(regexpMatchTimeoutSeconds) * time.Second
return regexp, nil
}