Skip to content

Commit 195ecb0

Browse files
committed
feat: registry error details
Signed-off-by: Timur Tuktamyshev <timur.tuktamyshev@flant.com>
1 parent c55e779 commit 195ecb0

3 files changed

Lines changed: 291 additions & 4 deletions

File tree

internal/mirror/cmd/pull/pull.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import (
4949
"github.com/deckhouse/deckhouse-cli/internal/version"
5050
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/modules"
5151
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/operations/params"
52+
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/errorutil"
5253
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
5354
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/validation"
5455
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
@@ -307,7 +308,8 @@ func (p *Puller) Execute(ctx context.Context) error {
307308
p.logger.WarnLn("Operation cancelled by user")
308309
return nil
309310
}
310-
return fmt.Errorf("pull from registry: %w", err)
311+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
312+
return fmt.Errorf("pull from registry failed: %w", err)
311313
}
312314

313315
return nil
@@ -420,6 +422,7 @@ func (p *Puller) validatePlatformAccess() error {
420422
}
421423

422424
if accessErr != nil {
425+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(accessErr))
423426
return fmt.Errorf("Source registry is not accessible: %w", accessErr)
424427
}
425428

@@ -472,6 +475,7 @@ func (p *Puller) pullSecurityDatabases() error {
472475
p.logger.Warnf("Skipping pull of security databases: %v", err)
473476
return nil
474477
case err != nil:
478+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
475479
return fmt.Errorf("Source registry is not accessible: %w", err)
476480
}
477481

@@ -547,6 +551,7 @@ func (p *Puller) validateModulesAccess() error {
547551
defer cancel()
548552

549553
if err := p.accessValidator.ValidateListAccessForRepo(ctx, modulesRepo, p.validationOpts...); err != nil {
554+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
550555
return fmt.Errorf("Source registry is not accessible: %w", err)
551556
}
552557
return nil

internal/mirror/cmd/push/push.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/deckhouse/deckhouse-cli/internal/mirror/operations"
4444
"github.com/deckhouse/deckhouse-cli/internal/version"
4545
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/operations/params"
46+
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/errorutil"
4647
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
4748
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/validation"
4849
)
@@ -236,6 +237,7 @@ func validateRegistryAccess(ctx context.Context, pushParams *params.PushParams)
236237
accessValidator := validation.NewRemoteRegistryAccessValidator()
237238
err := accessValidator.ValidateWriteAccessForRepo(ctx, path.Join(pushParams.RegistryHost, pushParams.RegistryPath), opts...)
238239
if err != nil {
240+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
239241
return fmt.Errorf("validate write access to registry %s: %w", path.Join(pushParams.RegistryHost, pushParams.RegistryPath), err)
240242
}
241243

@@ -359,7 +361,8 @@ func (p *Pusher) executeNewPush() error {
359361
p.logger.WarnLn("Operation cancelled by user")
360362
return nil
361363
}
362-
return fmt.Errorf("push to registry: %w", err)
364+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
365+
return fmt.Errorf("push to registry failed: %w", err)
363366
}
364367

365368
return nil
@@ -372,7 +375,8 @@ func (p *Pusher) validateRegistryAccess() error {
372375
defer cancel()
373376
err := validateRegistryAccess(ctx, p.pushParams)
374377
if err != nil && os.Getenv("MIRROR_BYPASS_ACCESS_CHECKS") != "1" {
375-
return fmt.Errorf("registry credentials validation failure: %w", err)
378+
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
379+
return fmt.Errorf("registry credentials validation failed: %w", err)
376380
}
377381
return nil
378382
}

pkg/libmirror/util/errorutil/errors.go

Lines changed: 279 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ limitations under the License.
1616

1717
package errorutil
1818

19-
import "strings"
19+
import (
20+
"crypto/x509"
21+
"errors"
22+
"fmt"
23+
"net"
24+
"net/http"
25+
"strings"
26+
"syscall"
27+
28+
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
29+
)
2030

2131
const CustomTrivyMediaTypesWarning = `` +
2232
"It looks like you are using Project Quay registry and it is not configured correctly for hosting Deckhouse.\n" +
@@ -58,3 +68,271 @@ func IsTrivyMediaTypeNotAllowedError(err error) bool {
5868
return strings.Contains(errMsg, "MANIFEST_INVALID") &&
5969
(strings.Contains(errMsg, "vnd.aquasec.trivy") || strings.Contains(errMsg, "application/octet-stream"))
6070
}
71+
72+
const (
73+
colorReset = "\033[0m"
74+
colorRed = "\033[31m"
75+
colorYellow = "\033[33m"
76+
colorCyan = "\033[36m"
77+
colorBold = "\033[1m"
78+
)
79+
80+
type errorCategory struct {
81+
name string
82+
causes []string
83+
solutions []string
84+
}
85+
86+
func isCertificateError(err error) bool {
87+
var (
88+
unknownAuthErr x509.UnknownAuthorityError
89+
certInvalidErr x509.CertificateInvalidError
90+
hostnameErr x509.HostnameError
91+
systemRootsErr x509.SystemRootsError
92+
constraintErr x509.ConstraintViolationError
93+
insecureAlgErr x509.InsecureAlgorithmError
94+
)
95+
96+
return errors.As(err, &unknownAuthErr) ||
97+
errors.As(err, &certInvalidErr) ||
98+
errors.As(err, &hostnameErr) ||
99+
errors.As(err, &systemRootsErr) ||
100+
errors.As(err, &constraintErr) ||
101+
errors.As(err, &insecureAlgErr)
102+
}
103+
104+
func isAuthenticationError(err error) bool {
105+
var transportErr *transport.Error
106+
if !errors.As(err, &transportErr) {
107+
return false
108+
}
109+
110+
if transportErr.StatusCode == http.StatusUnauthorized || transportErr.StatusCode == http.StatusForbidden {
111+
return true
112+
}
113+
114+
for _, diag := range transportErr.Errors {
115+
if diag.Code == transport.UnauthorizedErrorCode || diag.Code == transport.DeniedErrorCode {
116+
return true
117+
}
118+
}
119+
120+
return false
121+
}
122+
123+
func isNetworkError(err error) bool {
124+
var (
125+
netErr net.Error
126+
opErr *net.OpError
127+
syscallErr syscall.Errno
128+
)
129+
130+
if errors.As(err, &netErr) {
131+
return true
132+
}
133+
134+
if errors.As(err, &opErr) {
135+
return true
136+
}
137+
138+
if errors.As(err, &syscallErr) {
139+
return syscallErr == syscall.ECONNREFUSED ||
140+
syscallErr == syscall.ECONNRESET ||
141+
syscallErr == syscall.ETIMEDOUT ||
142+
syscallErr == syscall.ENETUNREACH ||
143+
syscallErr == syscall.EHOSTUNREACH
144+
}
145+
146+
return false
147+
}
148+
149+
func isDNSError(err error) bool {
150+
var dnsErr *net.DNSError
151+
return errors.As(err, &dnsErr)
152+
}
153+
154+
func formatError(category errorCategory, err error) string {
155+
var b strings.Builder
156+
157+
b.WriteString("\n")
158+
b.WriteString(colorBold)
159+
b.WriteString(colorRed)
160+
b.WriteString("error")
161+
b.WriteString(colorReset)
162+
b.WriteString(colorBold)
163+
b.WriteString(": ")
164+
b.WriteString(category.name)
165+
b.WriteString(colorReset)
166+
b.WriteString("\n")
167+
168+
b.WriteString(colorCyan)
169+
b.WriteString(" ╰─▶ ")
170+
b.WriteString(colorReset)
171+
b.WriteString(err.Error())
172+
b.WriteString("\n\n")
173+
174+
if len(category.causes) > 0 {
175+
b.WriteString(colorYellow)
176+
b.WriteString(" Possible causes:\n")
177+
b.WriteString(colorReset)
178+
for _, cause := range category.causes {
179+
b.WriteString(" • ")
180+
b.WriteString(cause)
181+
b.WriteString("\n")
182+
}
183+
b.WriteString("\n")
184+
}
185+
186+
if len(category.solutions) > 0 {
187+
b.WriteString(colorCyan)
188+
b.WriteString(" How to fix:\n")
189+
b.WriteString(colorReset)
190+
for _, solution := range category.solutions {
191+
b.WriteString(" • ")
192+
b.WriteString(solution)
193+
b.WriteString("\n")
194+
}
195+
}
196+
197+
return b.String()
198+
}
199+
200+
func FormatRegistryError(err error) string {
201+
if err == nil {
202+
return ""
203+
}
204+
205+
var category errorCategory
206+
207+
switch {
208+
case isCertificateError(err):
209+
category = errorCategory{
210+
name: "TLS/certificate verification failed",
211+
causes: []string{
212+
"Self-signed certificate without proper trust chain",
213+
"Certificate expired or not yet valid",
214+
"Hostname mismatch between certificate and registry URL",
215+
"Corporate proxy or middleware intercepting HTTPS connections",
216+
},
217+
solutions: []string{
218+
"Use --insecure flag to skip TLS verification (not recommended for production)",
219+
"Add the registry's CA certificate to your system trust store",
220+
"Verify the registry URL hostname matches the certificate",
221+
},
222+
}
223+
224+
case isAuthenticationError(err):
225+
var transportErr *transport.Error
226+
name := "Authentication failed"
227+
if errors.As(err, &transportErr) {
228+
switch transportErr.StatusCode {
229+
case http.StatusUnauthorized:
230+
name = "Authentication failed (HTTP 401 Unauthorized)"
231+
case http.StatusForbidden:
232+
name = "Access denied (HTTP 403 Forbidden)"
233+
}
234+
}
235+
236+
category = errorCategory{
237+
name: name,
238+
causes: []string{
239+
"Invalid or expired credentials",
240+
"License key is incorrect, expired, or not provided",
241+
"Insufficient permissions for the requested operation",
242+
},
243+
solutions: []string{
244+
"Verify your license key is correct and not expired",
245+
"Ensure --license flag is specified with a valid key",
246+
"Contact registry administrator to verify access rights",
247+
},
248+
}
249+
250+
case isDNSError(err):
251+
var dnsErr *net.DNSError
252+
name := "DNS resolution failed"
253+
if errors.As(err, &dnsErr) && dnsErr.Name != "" {
254+
name = fmt.Sprintf("DNS resolution failed for '%s'", dnsErr.Name)
255+
}
256+
257+
category = errorCategory{
258+
name: name,
259+
causes: []string{
260+
"Registry hostname cannot be resolved by DNS",
261+
"DNS server is unreachable or not responding",
262+
"Incorrect registry URL or typo in hostname",
263+
},
264+
solutions: []string{
265+
"Verify the registry URL is spelled correctly",
266+
"Check your DNS server configuration",
267+
"Try using the registry's IP address instead of hostname",
268+
},
269+
}
270+
271+
case isNetworkError(err):
272+
var opErr *net.OpError
273+
name := "Network connection failed"
274+
if errors.As(err, &opErr) {
275+
if opErr.Addr != nil {
276+
name = fmt.Sprintf("Network connection failed to %s", opErr.Addr.String())
277+
}
278+
}
279+
280+
category = errorCategory{
281+
name: name,
282+
causes: []string{
283+
"Network connectivity issues or no internet connection",
284+
"Firewall or security group blocking the connection",
285+
"Registry server is down or unreachable",
286+
},
287+
solutions: []string{
288+
"Check your network connection and internet access",
289+
"Verify firewall rules allow outbound HTTPS (port 443)",
290+
"Test connectivity with: curl -v https://<registry>",
291+
},
292+
}
293+
294+
case IsImageNotFoundError(err):
295+
category = errorCategory{
296+
name: "Image not found in registry",
297+
causes: []string{
298+
"Image tag doesn't exist in the registry",
299+
"Incorrect image name or tag specified",
300+
},
301+
solutions: []string{
302+
"Verify the image name and tag are correct",
303+
"Check if you have permission to access this image",
304+
},
305+
}
306+
307+
case IsRepoNotFoundError(err):
308+
category = errorCategory{
309+
name: "Repository not found in registry",
310+
causes: []string{
311+
"Repository doesn't exist in the registry",
312+
"Incorrect repository path or name",
313+
},
314+
solutions: []string{
315+
"Verify the repository path is correct",
316+
"Ensure you have permission to access this repository",
317+
},
318+
}
319+
320+
case IsTrivyMediaTypeNotAllowedError(err):
321+
category = errorCategory{
322+
name: "Unsupported OCI artifact type",
323+
causes: []string{
324+
"Registry doesn't support required media types for Trivy security databases",
325+
"Project Quay registry not configured for Deckhouse artifacts",
326+
},
327+
solutions: []string{
328+
"Configure registry to allow custom OCI artifact types",
329+
"See: https://deckhouse.io/products/kubernetes-platform/documentation/v1/supported_versions.html#container-registry",
330+
},
331+
}
332+
333+
default:
334+
return err.Error()
335+
}
336+
337+
return formatError(category, err)
338+
}

0 commit comments

Comments
 (0)