Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion internal/mirror/cmd/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/deckhouse/deckhouse-cli/internal/version"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/modules"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/operations/params"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/errorutil"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/validation"
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
Expand Down Expand Up @@ -307,7 +308,8 @@ func (p *Puller) Execute(ctx context.Context) error {
p.logger.WarnLn("Operation cancelled by user")
return nil
}
return fmt.Errorf("pull from registry: %w", err)
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
return fmt.Errorf("pull from registry failed: %w", err)
}

return nil
Expand Down Expand Up @@ -420,6 +422,7 @@ func (p *Puller) validatePlatformAccess() error {
}

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

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

Expand Down Expand Up @@ -547,6 +551,7 @@ func (p *Puller) validateModulesAccess() error {
defer cancel()

if err := p.accessValidator.ValidateListAccessForRepo(ctx, modulesRepo, p.validationOpts...); err != nil {
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
return fmt.Errorf("Source registry is not accessible: %w", err)
}
return nil
Expand Down
8 changes: 6 additions & 2 deletions internal/mirror/cmd/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/deckhouse/deckhouse-cli/internal/mirror/operations"
"github.com/deckhouse/deckhouse-cli/internal/version"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/operations/params"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/errorutil"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/validation"
)
Expand Down Expand Up @@ -236,6 +237,7 @@ func validateRegistryAccess(ctx context.Context, pushParams *params.PushParams)
accessValidator := validation.NewRemoteRegistryAccessValidator()
err := accessValidator.ValidateWriteAccessForRepo(ctx, path.Join(pushParams.RegistryHost, pushParams.RegistryPath), opts...)
if err != nil {
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
return fmt.Errorf("validate write access to registry %s: %w", path.Join(pushParams.RegistryHost, pushParams.RegistryPath), err)
}

Expand Down Expand Up @@ -359,7 +361,8 @@ func (p *Pusher) executeNewPush() error {
p.logger.WarnLn("Operation cancelled by user")
return nil
}
return fmt.Errorf("push to registry: %w", err)
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
return fmt.Errorf("push to registry failed: %w", err)
}

return nil
Expand All @@ -372,7 +375,8 @@ func (p *Pusher) validateRegistryAccess() error {
defer cancel()
err := validateRegistryAccess(ctx, p.pushParams)
if err != nil && os.Getenv("MIRROR_BYPASS_ACCESS_CHECKS") != "1" {
return fmt.Errorf("registry credentials validation failure: %w", err)
fmt.Fprint(os.Stderr, errorutil.FormatRegistryError(err))
return fmt.Errorf("registry credentials validation failed: %w", err)
}
return nil
}
Expand Down
280 changes: 279 additions & 1 deletion pkg/libmirror/util/errorutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ limitations under the License.

package errorutil

import "strings"
import (
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"strings"
"syscall"

"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)

const CustomTrivyMediaTypesWarning = `` +
"It looks like you are using Project Quay registry and it is not configured correctly for hosting Deckhouse.\n" +
Expand Down Expand Up @@ -58,3 +68,271 @@ func IsTrivyMediaTypeNotAllowedError(err error) bool {
return strings.Contains(errMsg, "MANIFEST_INVALID") &&
(strings.Contains(errMsg, "vnd.aquasec.trivy") || strings.Contains(errMsg, "application/octet-stream"))
}

const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorYellow = "\033[33m"
colorCyan = "\033[36m"
colorBold = "\033[1m"
)

type errorCategory struct {
name string
causes []string
solutions []string
}

func isCertificateError(err error) bool {
var (
unknownAuthErr x509.UnknownAuthorityError
certInvalidErr x509.CertificateInvalidError
hostnameErr x509.HostnameError
systemRootsErr x509.SystemRootsError
constraintErr x509.ConstraintViolationError
insecureAlgErr x509.InsecureAlgorithmError
)

return errors.As(err, &unknownAuthErr) ||
errors.As(err, &certInvalidErr) ||
errors.As(err, &hostnameErr) ||
errors.As(err, &systemRootsErr) ||
errors.As(err, &constraintErr) ||
errors.As(err, &insecureAlgErr)
}

func isAuthenticationError(err error) bool {
var transportErr *transport.Error
if !errors.As(err, &transportErr) {
return false
}

if transportErr.StatusCode == http.StatusUnauthorized || transportErr.StatusCode == http.StatusForbidden {
return true
}

for _, diag := range transportErr.Errors {
if diag.Code == transport.UnauthorizedErrorCode || diag.Code == transport.DeniedErrorCode {
return true
}
}

return false
}

func isNetworkError(err error) bool {
var (
netErr net.Error
opErr *net.OpError
syscallErr syscall.Errno
)

if errors.As(err, &netErr) {
return true
}

if errors.As(err, &opErr) {
return true
}

if errors.As(err, &syscallErr) {
return syscallErr == syscall.ECONNREFUSED ||
syscallErr == syscall.ECONNRESET ||
syscallErr == syscall.ETIMEDOUT ||
syscallErr == syscall.ENETUNREACH ||
syscallErr == syscall.EHOSTUNREACH
}

return false
}

func isDNSError(err error) bool {
var dnsErr *net.DNSError
return errors.As(err, &dnsErr)
}

func formatError(category errorCategory, err error) string {
var b strings.Builder

b.WriteString("\n")
b.WriteString(colorBold)
b.WriteString(colorRed)
b.WriteString("error")
b.WriteString(colorReset)
b.WriteString(colorBold)
b.WriteString(": ")
b.WriteString(category.name)
b.WriteString(colorReset)
b.WriteString("\n")

b.WriteString(colorCyan)
b.WriteString(" ╰─▶ ")
b.WriteString(colorReset)
b.WriteString(err.Error())
b.WriteString("\n\n")

if len(category.causes) > 0 {
b.WriteString(colorYellow)
b.WriteString(" Possible causes:\n")
b.WriteString(colorReset)
for _, cause := range category.causes {
b.WriteString(" • ")
b.WriteString(cause)
b.WriteString("\n")
}
b.WriteString("\n")
}

if len(category.solutions) > 0 {
b.WriteString(colorCyan)
b.WriteString(" How to fix:\n")
b.WriteString(colorReset)
for _, solution := range category.solutions {
b.WriteString(" • ")
b.WriteString(solution)
b.WriteString("\n")
}
}

return b.String()
}

func FormatRegistryError(err error) string {
if err == nil {
return ""
}

var category errorCategory

switch {
case isCertificateError(err):
category = errorCategory{
name: "TLS/certificate verification failed",
causes: []string{
"Self-signed certificate without proper trust chain",
"Certificate expired or not yet valid",
"Hostname mismatch between certificate and registry URL",
"Corporate proxy or middleware intercepting HTTPS connections",
},
solutions: []string{
"Use --insecure flag to skip TLS verification (not recommended for production)",
"Add the registry's CA certificate to your system trust store",
"Verify the registry URL hostname matches the certificate",
},
}

case isAuthenticationError(err):
var transportErr *transport.Error
name := "Authentication failed"
if errors.As(err, &transportErr) {
switch transportErr.StatusCode {
case http.StatusUnauthorized:
name = "Authentication failed (HTTP 401 Unauthorized)"
case http.StatusForbidden:
name = "Access denied (HTTP 403 Forbidden)"
}
}

category = errorCategory{
name: name,
causes: []string{
"Invalid or expired credentials",
"License key is incorrect, expired, or not provided",
"Insufficient permissions for the requested operation",
},
solutions: []string{
"Verify your license key is correct and not expired",
"Ensure --license flag is specified with a valid key",
"Contact registry administrator to verify access rights",
},
}

case isDNSError(err):
var dnsErr *net.DNSError
name := "DNS resolution failed"
if errors.As(err, &dnsErr) && dnsErr.Name != "" {
name = fmt.Sprintf("DNS resolution failed for '%s'", dnsErr.Name)
}

category = errorCategory{
name: name,
causes: []string{
"Registry hostname cannot be resolved by DNS",
"DNS server is unreachable or not responding",
"Incorrect registry URL or typo in hostname",
},
solutions: []string{
"Verify the registry URL is spelled correctly",
"Check your DNS server configuration",
"Try using the registry's IP address instead of hostname",
},
}

case isNetworkError(err):
var opErr *net.OpError
name := "Network connection failed"
if errors.As(err, &opErr) {
if opErr.Addr != nil {
name = fmt.Sprintf("Network connection failed to %s", opErr.Addr.String())
}
}

category = errorCategory{
name: name,
causes: []string{
"Network connectivity issues or no internet connection",
"Firewall or security group blocking the connection",
"Registry server is down or unreachable",
},
solutions: []string{
"Check your network connection and internet access",
"Verify firewall rules allow outbound HTTPS (port 443)",
"Test connectivity with: curl -v https://<registry>",
},
}

case IsImageNotFoundError(err):
category = errorCategory{
name: "Image not found in registry",
causes: []string{
"Image tag doesn't exist in the registry",
"Incorrect image name or tag specified",
},
solutions: []string{
"Verify the image name and tag are correct",
"Check if you have permission to access this image",
},
}

case IsRepoNotFoundError(err):
category = errorCategory{
name: "Repository not found in registry",
causes: []string{
"Repository doesn't exist in the registry",
"Incorrect repository path or name",
},
solutions: []string{
"Verify the repository path is correct",
"Ensure you have permission to access this repository",
},
}

case IsTrivyMediaTypeNotAllowedError(err):
category = errorCategory{
name: "Unsupported OCI artifact type",
causes: []string{
"Registry doesn't support required media types for Trivy security databases",
"Project Quay registry not configured for Deckhouse artifacts",
},
solutions: []string{
"Configure registry to allow custom OCI artifact types",
"See: https://deckhouse.io/products/kubernetes-platform/documentation/v1/supported_versions.html#container-registry",
},
}

default:
return err.Error()
}

return formatError(category, err)
}