Skip to content
Merged
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
214 changes: 177 additions & 37 deletions pkg/modules/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While Credential and InsecureSkipVerify are now correctly configured using selectedAuth, the plainHTTP setting is determined later on line 208 using plainHTTPFunc(img, i.auths, false), which still contains the old logic that doesn't respect longest-prefix matching. For consistency and correctness, plainHTTPFunc should be refactored to accept selectedAuth (similar to the change made for skipTLSVerifyFunc), and then used to set src.PlainHTTP.

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)
Expand All @@ -204,7 +206,7 @@ func (i imagePullArgs) pull(ctx context.Context, platform string) error {
}
}

src.PlainHTTP = plainHTTPFunc(img, i.auths, false)
src.PlainHTTP = plainHTTPFunc(selectedAuth, false)

if _, err = oras.Copy(ctx, src, src.Reference.Reference, dst, "", copyOption); err != nil {
return errors.Wrapf(err, "failed to pull image %q to local dir", img)
Expand All @@ -228,50 +230,187 @@ 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
}
return auth.Credential{
Username: selectedAuth.Username,
Password: selectedAuth.Password,
}, nil
}
}

func selectAuth(image string, authList []imageAuth) *imageAuth {
// remove tag and hash
repoPart := extractRepoFromImage(image)

// 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 = -1

// find biggest match point auth
for i := range authList {
authRepo := authList[i].Repo

authURL, err := normalizeImageToURL(authRepo)
if err != nil {
continue
}
creds[rp] = auth.Credential{
Username: inputAuth.Username,
Password: inputAuth.Password,

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 func(_ context.Context, hostport string) (auth.Credential, error) {
cred, ok := creds[hostport]
if !ok {
cred = auth.EmptyCredential

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 {
if image == "" {
return ""
}

repoPart := image

// handle image with hash
atIdx := strings.LastIndex(repoPart, "@")
if atIdx != -1 && atIdx > 0 {
afterAt := repoPart[atIdx+1:]
if isLikelyDigest(afterAt) {
repoPart = repoPart[:atIdx]
}
}

// search /
firstSlashIdx := strings.Index(repoPart, "/")

if firstSlashIdx == -1 {
return repoPart
}

// search : for tag or port
lastColonIdx := strings.LastIndex(repoPart, ":")
if lastColonIdx == -1 {
return repoPart
}

if lastColonIdx < firstSlashIdx {
return repoPart
}

if lastColonIdx+1 < len(repoPart) {
afterColon := repoPart[lastColonIdx+1:]
if strings.Contains(afterColon, "/") {
return repoPart
}
return cred, nil
}
return repoPart[:lastColonIdx]
}

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 isLikelyDigest(s string) bool {
colonIdx := strings.Index(s, ":")
if colonIdx == -1 {
return false
}
algorithm := s[:colonIdx]
knownAlgorithms := []string{"sha256", "sha512", "sha384", "sha1", "md5"}
for _, algo := range knownAlgorithms {
if algorithm == algo {
return true
}
}
return defaults
return false
}

func plainHTTPFunc(img string, auths []imageAuth, defaults bool) bool {
imgHost := strings.Split(img, "/")[0]
for _, a := range auths {
if imgHost == a.Repo {
if a.PlainHTTP != nil {
return *a.PlainHTTP
}
return defaults
// 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 {
return 1
}
score = 2
} 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
}

func plainHTTPFunc(selectedAuth *imageAuth, defaults bool) bool {
if selectedAuth != nil && selectedAuth.PlainHTTP != nil {
return *selectedAuth.PlainHTTP
}
return defaults
}

Expand Down Expand Up @@ -337,19 +476,20 @@ 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the pull function, while Credential and InsecureSkipVerify are now correctly configured using selectedAuth, the plainHTTP setting is determined later on line 356 using plainHTTPFunc(dest, i.auths, false), which still contains the old logic. For consistency and correctness, plainHTTPFunc should be refactored to accept selectedAuth and be used to set dst.PlainHTTP.

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)
dst.PlainHTTP = plainHTTPFunc(selectedAuth, false)

if _, err = oras.Copy(ctx, src, src.Reference.Reference, dst, dst.Reference.Reference, oras.DefaultCopyOptions); err != nil {
return errors.Wrapf(err, "failed to push image %q to remote", img)
Expand Down