8
8
9
9
"github.com/containers/image/v5/docker"
10
10
"github.com/docker/distribution/registry/api/errcode"
11
- errcodev2 "github.com/docker/distribution/registry/api/v2"
11
+ v2 "github.com/docker/distribution/registry/api/v2"
12
12
)
13
13
14
14
// IsTolerableDeleteErr determines if the returned error message during image deletion can be
@@ -24,23 +24,59 @@ func IsTolerableDeleteErr(err error) bool {
24
24
return false
25
25
}
26
26
27
- // Any errors related to the actual image registry query are wrapped in an
28
- // ErrImage instance. This allows us to easily identify intolerable errors
29
- // such as not being able to write the authfile or certs, etc.
30
- var errImage * ErrImage
31
- if ! errors .As (err , & errImage ) {
27
+ if IsImageNotFoundErr (err ) {
28
+ return true
29
+ }
30
+
31
+ if IsAccessDeniedErr (err ) {
32
+ return true
33
+ }
34
+
35
+ return false
36
+ }
37
+
38
+ // IsImageNotFoundErr determines if the returned error message indicates that
39
+ // the image is not found. This assumes that the image registry returns such an
40
+ // error code, which is not always the case. Some image registries return an
41
+ // unauthorized or forbidden error which this function does not take into
42
+ // account. For that, use IsAccessDeniedErr below as an additional check.
43
+ func IsImageNotFoundErr (err error ) bool {
44
+ if err == nil {
32
45
return false
33
46
}
34
47
35
- if isTolerableErrorCode (err ) {
48
+ if ! isErrImage (err ) {
49
+ return false
50
+ }
51
+
52
+ if isMaskedHTTP404 (err ) {
36
53
return true
37
54
}
38
55
39
- if isTolerableUnexpectedHTTPStatusError (err ) {
56
+ if isImageNotFoundErrorCode (err ) {
40
57
return true
41
58
}
42
59
43
- if isMaskedHTTP404 (err ) {
60
+ return false
61
+ }
62
+
63
+ // IsAccessDeniedErr determines if the returned error indicates that the image
64
+ // cannot be accessed or the operation cannot be performed due to permissions
65
+ // issue. Some image registries use this as a proxy for the image not existing.
66
+ func IsAccessDeniedErr (err error ) bool {
67
+ if err == nil {
68
+ return false
69
+ }
70
+
71
+ if ! isErrImage (err ) {
72
+ return false
73
+ }
74
+
75
+ if isAccessDeniedErrorCode (err ) {
76
+ return true
77
+ }
78
+
79
+ if isTolerableUnexpectedHTTPStatusError (err ) {
44
80
return true
45
81
}
46
82
@@ -59,37 +95,80 @@ func isMaskedHTTP404(err error) bool {
59
95
return strings .Contains (err .Error (), "Image may not exist or is not stored with a v2 Schema in a v2 registry" )
60
96
}
61
97
62
- // isTolerableErrorCode checks if the error code from the registry is tolerable for deletion.
63
- // This includes cases where the manifest is unknown, or authorization is denied.
64
- func isTolerableErrorCode (err error ) bool {
98
+ // isImageNotFoundErrorCode checks if the error is an ErrorCode instance
99
+ // indicating that a given manifest is not found or the repo name is unknown.
100
+ // This also handles the Quay.io edgecase of an image being deleted and Quay
101
+ // returning an HTTP 500 indicating that.
102
+ func isImageNotFoundErrorCode (err error ) bool {
65
103
if err == nil {
66
104
return false
67
105
}
68
106
107
+ if isManifestUnknownError (err ) {
108
+ return true
109
+ }
110
+
111
+ if isNameUnknownError (err ) {
112
+ return true
113
+ }
114
+
69
115
var errCode errcode.Error
70
- if ! errors .As (err , & errCode ) {
71
- return false
116
+ if errors .As (err , & errCode ) {
117
+ return isQuayErrorCode ( errCode )
72
118
}
73
119
74
- code := errCode .ErrorCode ()
120
+ return false
121
+ }
75
122
76
- if code == errcodev2 .ErrorCodeManifestUnknown {
123
+ // Determines if the error is due to the repo name being unknown.
124
+ func isNameUnknownError (err error ) bool {
125
+ var ec errcode.ErrorCoder
126
+ if errors .As (err , & ec ) && ec .ErrorCode () == v2 .ErrorCodeNameUnknown {
77
127
return true
78
128
}
79
129
80
- // Quay.io returns this code whenever one is not authorized to delete an image.
81
- if code == errcode .ErrorCodeUnauthorized {
130
+ return false
131
+ }
132
+
133
+ // Adapted from: https://github.com/containers/image/blob/52ee4dff559a09ffa45783c50bcb7b3f7faebb04/docker/docker_client.go#L1109-L1133
134
+ func isManifestUnknownError (err error ) bool {
135
+ // docker/distribution, and as defined in the spec
136
+ var ec errcode.ErrorCoder
137
+ if errors .As (err , & ec ) && ec .ErrorCode () == v2 .ErrorCodeManifestUnknown {
138
+ return true
139
+ }
140
+ // registry.redhat.io as of October 2022
141
+ var e errcode.Error
142
+ if errors .As (err , & e ) && e .ErrorCode () == errcode .ErrorCodeUnknown && e .Message == "Not Found" {
143
+ return true
144
+ }
145
+ // Harbor v2.10.2
146
+ if errors .As (err , & e ) && e .ErrorCode () == errcode .ErrorCodeUnknown && strings .Contains (strings .ToLower (e .Message ), "not found" ) {
147
+ return true
148
+ }
149
+ // registry.access.redhat.com as of August 2025
150
+ if errors .As (err , & e ) && e .ErrorCode () == v2 .ErrorCodeNameUnknown {
82
151
return true
83
152
}
84
153
85
- // Docker.io returns this code whenever one is not authorized to delete an image.
86
- if code == errcode .ErrorCodeDenied {
154
+ return false
155
+ }
156
+
157
+ // isAccessDeniedErrorCode checks if the error is an ErrorCode instance and
158
+ // then checks the known status codes.
159
+ func isAccessDeniedErrorCode (err error ) bool {
160
+ if err == nil {
161
+ return false
162
+ }
163
+
164
+ var ec errcode.ErrorCoder
165
+ // Quay.io returns this code whenever one is not authorized to delete an image.
166
+ if errors .As (err , & ec ) && ec .ErrorCode () == errcode .ErrorCodeUnauthorized {
87
167
return true
88
168
}
89
169
90
- // Quay.io returns an HTTP 500 if an image was recently deleted and the
91
- // garbage collection has not run yet.
92
- if isQuayErrorCode (errCode ) {
170
+ // Docker.io returns this code whenever one is not authorized to delete an image.
171
+ if errors .As (err , & ec ) && ec .ErrorCode () == errcode .ErrorCodeDenied {
93
172
return true
94
173
}
95
174
@@ -128,15 +207,16 @@ func isTolerableUnexpectedHTTPStatusError(err error) bool {
128
207
}
129
208
130
209
var unexpectedHTTPErr docker.UnexpectedHTTPStatusError
131
- if ! errors .As (err , & unexpectedHTTPErr ) {
132
- return false
210
+ if errors .As (err , & unexpectedHTTPErr ) && unexpectedHTTPErr . StatusCode == http . StatusUnauthorized {
211
+ return true
133
212
}
134
213
135
- if unexpectedHTTPErr .StatusCode == http .StatusUnauthorized {
214
+ if errors . As ( err , & unexpectedHTTPErr ) && unexpectedHTTPErr .StatusCode == http .StatusForbidden {
136
215
return true
137
216
}
138
217
139
- if unexpectedHTTPErr .StatusCode == http .StatusForbidden {
218
+ var unauthedForCreds docker.ErrUnauthorizedForCredentials
219
+ if errors .As (err , & unauthedForCreds ) {
140
220
return true
141
221
}
142
222
@@ -189,3 +269,17 @@ func (e *ErrImage) Error() string {
189
269
func (e * ErrImage ) Unwrap () error {
190
270
return e .err
191
271
}
272
+
273
+ // isErrImage determines whether the given error is an instance of the ErrImage
274
+ // type defined above.
275
+ func isErrImage (err error ) bool {
276
+ if err == nil {
277
+ return false
278
+ }
279
+
280
+ // Any errors related to the actual image registry query are wrapped in an
281
+ // ErrImage instance. This allows us to easily identify intolerable errors
282
+ // such as not being able to write the authfile or certs, etc.
283
+ var errImage * ErrImage
284
+ return errors .As (err , & errImage )
285
+ }
0 commit comments