From 5e8a9e705e7dde0e7f5a97e9897c6bd6fab40d04 Mon Sep 17 00:00:00 2001 From: "xuesongzuo@yunify.com" Date: Thu, 18 Dec 2025 10:18:52 +0800 Subject: [PATCH] bugfix : fix image auth bug Signed-off-by: xuesongzuo@yunify.com bugfix : fix image auth bug Signed-off-by: xuesongzuo@yunify.com --- pkg/modules/image.go | 184 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 155 insertions(+), 29 deletions(-) diff --git a/pkg/modules/image.go b/pkg/modules/image.go index 5e67e238..4ec8313f 100644 --- a/pkg/modules/image.go +++ b/pkg/modules/image.go @@ -25,6 +25,7 @@ import ( "io" "io/fs" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -177,16 +178,17 @@ func (i imagePullArgs) pull(ctx context.Context, platform string) error { if err != nil { return errors.Wrapf(err, "failed to get remote image %s", img) } + selectedAuth := selectAuth(img, i.auths) src.Client = &auth.Client{ Client: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: skipTLSVerifyFunc(img, i.auths, *i.skipTLSVerify), + InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify), }, }, }, Cache: auth.NewCache(), - Credential: authFunc(i.auths), + Credential: authFunc(selectedAuth), } dst, err := newLocalRepository(filepath.Join(src.Reference.Registry, src.Reference.Repository)+":"+src.Reference.Reference, i.imagesDir) @@ -228,36 +230,159 @@ func dockerHostParser(img string) string { return strings.Join(splitedImg, "/") } -func authFunc(auths []imageAuth) func(ctx context.Context, hostport string) (auth.Credential, error) { - var creds = make(map[string]auth.Credential) - for _, inputAuth := range auths { - var rp = inputAuth.Repo - if rp == "docker.io" || rp == "" { - rp = "registry-1.docker.io" +func authFunc(selectedAuth *imageAuth) func(ctx context.Context, hostport string) (auth.Credential, error) { + return func(_ context.Context, _ string) (auth.Credential, error) { + if selectedAuth == nil { + return auth.Credential{ + Username: "", + Password: "", + }, nil } - creds[rp] = auth.Credential{ - Username: inputAuth.Username, - Password: inputAuth.Password, - } - } - return func(_ context.Context, hostport string) (auth.Credential, error) { - cred, ok := creds[hostport] - if !ok { - cred = auth.EmptyCredential - } - return cred, nil + return auth.Credential{ + Username: selectedAuth.Username, + Password: selectedAuth.Password, + }, nil } } -func skipTLSVerifyFunc(img string, auths []imageAuth, defaults bool) bool { - imgHost := strings.Split(img, "/")[0] - for _, a := range auths { - if imgHost == a.Repo { - if a.Insecure != nil { - return *a.Insecure - } - return defaults +func selectAuth(image string, authList []imageAuth) *imageAuth { + // remove tag and hash + repoPart, err := extractRepoFromImage(image) + if err != nil { + return nil + } + + // parse image to url + imageURL, err := normalizeImageToURL(repoPart) + if err != nil { + return nil + } + + imageHost := imageURL.Host + imagePath := strings.TrimPrefix(imageURL.Path, "/") + + // split port + imageHostWithoutPort := imageHost + if colonIdx := strings.Index(imageHost, ":"); colonIdx != -1 { + imageHostWithoutPort = imageHost[:colonIdx] + } + + var bestMatch *imageAuth + var bestMatchScore int = -1 + + // find biggest match point auth + for i := range authList { + authRepo := authList[i].Repo + + authURL, err := normalizeImageToURL(authRepo) + if err != nil { + continue } + + authHost := authURL.Host + authPath := strings.TrimPrefix(authURL.Path, "/") + + authHostWithoutPort := authHost + if colonIdx := strings.Index(authHost, ":"); colonIdx != -1 { + authHostWithoutPort = authHost[:colonIdx] + } + + score := calculateMatchScore(imageHost, imageHostWithoutPort, imagePath, + authHost, authHostWithoutPort, authPath) + + if score > bestMatchScore { + bestMatchScore = score + bestMatch = &authList[i] + } + } + + return bestMatch +} + +func normalizeImageToURL(image string) (*url.URL, error) { + if strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") { + return url.Parse(image) + } + return url.Parse("http://" + image) +} + +// extractRepoFromImage handle image ,ignore tag and hash +// like xxx/xxx:tag@sha256:xxx to xxx/xxx +// like xxx/xxx:tag to xxx/xxx +func extractRepoFromImage(image string) (string, error) { + repoPart := image + + if atIdx := strings.LastIndex(image, "@"); atIdx != -1 { + repoPart = image[:atIdx] + } else { + colonIdx := strings.LastIndex(image, ":") + if colonIdx != -1 { + // check if : used for port + // if port , string after : must contain / + substr := image[:colonIdx] + lastSlashIdx := strings.LastIndex(substr, "/") + if lastSlashIdx != -1 { + repoPart = substr + } else { + if strings.Contains(substr, ".") || strings.Contains(substr, ":") { + } else { + repoPart = substr + } + } + } + } + + return repoPart, nil +} + +// calculateMatchScore calculate image and path match point +// 0 for not matched +// 1 for host matched +// 2 for host:port matched +// 3 for host:port and prefix path matched +// 4 for host:port and full path matched +func calculateMatchScore(imgHost, imgHostNoPort, imgPath, + authHost, authHostNoPort, authPath string) int { + + if imgHostNoPort != authHostNoPort { + return 0 + } + + score := 1 + + imgHasPort := strings.Contains(imgHost, ":") + authHasPort := strings.Contains(authHost, ":") + + if imgHasPort && authHasPort { + if imgHost == authHost { + score = 2 + } else { + return 1 + } + } else if !imgHasPort && !authHasPort { + score = 2 + } + + if authPath == "" { + return score + } + + if imgPath == authPath { + return score + 2 + } + + if strings.HasPrefix(imgPath, authPath) { + if len(imgPath) == len(authPath) || + (len(imgPath) > len(authPath) && imgPath[len(authPath)] == '/') { + return score + 1 + } + } + return 0 +} + +func skipTLSVerifyFunc(selectedAuth *imageAuth, defaults bool) bool { + if selectedAuth != nil && selectedAuth.Insecure != nil { + return *selectedAuth.Insecure } return defaults } @@ -337,16 +462,17 @@ func (i imagePushArgs) push(ctx context.Context, hostVars map[string]any) error if err != nil { return errors.Wrapf(err, "failed to get remote repository %q", dest) } + selectedAuth := selectAuth(dest, i.auths) dst.Client = &auth.Client{ Client: &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: skipTLSVerifyFunc(dest, i.auths, *i.skipTLSVerify), + InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify), }, }, }, Cache: auth.NewCache(), - Credential: authFunc(i.auths), + Credential: authFunc(selectedAuth), } dst.PlainHTTP = plainHTTPFunc(dest, i.auths, false)