-
Notifications
You must be signed in to change notification settings - Fork 132
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: replace Skopeo with Crane (#5692)
- Loading branch information
Showing
14 changed files
with
259 additions
and
365 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
package imageinspector | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/google/go-containerregistry/pkg/authn" | ||
"github.com/google/go-containerregistry/pkg/crane" | ||
corev1 "k8s.io/api/core/v1" | ||
|
||
"github.com/kubeshop/testkube/pkg/utils" | ||
) | ||
|
||
type craneFetcher struct { | ||
} | ||
|
||
func NewCraneFetcher() InfoFetcher { | ||
return &craneFetcher{} | ||
} | ||
|
||
func (c *craneFetcher) Fetch(ctx context.Context, registry, image string, pullSecrets []corev1.Secret) (*Info, error) { | ||
// If registry is not provided, extract it from the image name | ||
if registry == "" { | ||
registry = extractRegistry(image) | ||
} | ||
|
||
// If registry is provided via config and the image does not start with the registry, prepend it | ||
if registry != "" && registry != utils.DefaultDockerRegistry && !strings.HasPrefix(image, registry+"/") { | ||
image = registry + "/" + image | ||
} | ||
|
||
// Support pull secrets | ||
authConfigs, err := ParseSecretData(pullSecrets, registry) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Select the auth | ||
craneOptions := []crane.Option{crane.WithContext(ctx)} | ||
if len(authConfigs) > 0 { | ||
craneOptions = append(craneOptions, crane.WithAuth(authn.FromConfig(authConfigs[0]))) | ||
} | ||
|
||
// Fetch the image configuration | ||
fetchedAt := time.Now() | ||
serializedImageConfig, err := crane.Config(image, craneOptions...) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var imageConfig DockerImage | ||
if err = json.Unmarshal(serializedImageConfig, &imageConfig); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Build the required image information | ||
user, group := determineUserGroupPair(imageConfig.Config.User) | ||
result := &Info{ | ||
FetchedAt: fetchedAt, | ||
Entrypoint: imageConfig.Config.Entrypoint, | ||
Cmd: imageConfig.Config.Cmd, | ||
WorkingDir: imageConfig.Config.WorkingDir, | ||
User: user, | ||
Group: group, | ||
} | ||
|
||
// Try to detect optional shell information | ||
for i := len(imageConfig.History); i > 0; i-- { | ||
command := imageConfig.History[i-1].CreatedBy | ||
re, err := regexp.Compile(`/bin/([a-z]*)sh`) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result.Shell = re.FindString(command) | ||
if result.Shell != "" { | ||
break | ||
} | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
// DockerImage contains definition of docker image | ||
type DockerImage struct { | ||
Config struct { | ||
User string `json:"User"` | ||
Entrypoint []string `json:"Entrypoint"` | ||
Cmd []string `json:"Cmd"` | ||
WorkingDir string `json:"WorkingDir"` | ||
} `json:"config"` | ||
History []struct { | ||
Created time.Time `json:"created"` | ||
CreatedBy string `json:"created_by"` | ||
} `json:"history"` | ||
} | ||
|
||
// extractRegistry takes a container image string and returns the registry part. | ||
// It defaults to "docker.io" if no registry is specified. | ||
func extractRegistry(image string) string { | ||
parts := strings.Split(image, "/") | ||
// If the image is just a name, return the default registry. | ||
if len(parts) == 1 { | ||
return utils.DefaultDockerRegistry | ||
} | ||
// If the first part contains '.' or ':', it's likely a registry. | ||
if strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":") { | ||
return parts[0] | ||
} | ||
return utils.DefaultDockerRegistry | ||
} | ||
|
||
func determineUserGroupPair(userGroupStr string) (int64, int64) { | ||
if userGroupStr == "" { | ||
userGroupStr = "0" | ||
} | ||
userStr, groupStr, _ := strings.Cut(userGroupStr, ":") | ||
if groupStr == "" { | ||
groupStr = "0" | ||
} | ||
user, _ := strconv.Atoi(userStr) | ||
group, _ := strconv.Atoi(groupStr) | ||
return int64(user), int64(group) | ||
} | ||
|
||
// DockerAuths contains an embedded DockerAuthConfigs | ||
type DockerAuths struct { | ||
Auths map[string]authn.AuthConfig `json:"auths"` | ||
} | ||
|
||
// ParseSecretData parses secret data for docker auth config | ||
func ParseSecretData(imageSecrets []corev1.Secret, registry string) ([]authn.AuthConfig, error) { | ||
var results []authn.AuthConfig | ||
for _, imageSecret := range imageSecrets { | ||
auths := DockerAuths{} | ||
if jsonData, ok := imageSecret.Data[".dockerconfigjson"]; ok { | ||
if err := json.Unmarshal(jsonData, &auths); err != nil { | ||
return nil, err | ||
} | ||
} else if configData, ok := imageSecret.Data[".dockercfg"]; ok { | ||
if err := json.Unmarshal(configData, &auths.Auths); err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
return nil, fmt.Errorf("imagePullSecret %s contains neither .dockercfg nor .dockerconfigjson", imageSecret.Name) | ||
} | ||
|
||
// Determine if there is a secret for the specified registry | ||
if creds, ok := auths.Auths[registry]; ok { | ||
username, password, err := extractRegistryCredentials(creds) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
results = append(results, authn.AuthConfig{Username: username, Password: password}) | ||
} | ||
} | ||
|
||
return results, nil | ||
} | ||
|
||
func extractRegistryCredentials(creds authn.AuthConfig) (username, password string, err error) { | ||
if creds.Auth == "" { | ||
return creds.Username, creds.Password, nil | ||
} | ||
|
||
decoder := base64.StdEncoding | ||
if !strings.HasSuffix(strings.TrimSpace(creds.Auth), "=") { | ||
// Modify the decoder to be raw if no padding is present | ||
decoder = decoder.WithPadding(base64.NoPadding) | ||
} | ||
|
||
base64Decoded, err := decoder.DecodeString(creds.Auth) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
splitted := strings.SplitN(string(base64Decoded), ":", 2) | ||
if len(splitted) != 2 { | ||
return creds.Username, creds.Password, nil | ||
} | ||
|
||
return splitted[0], splitted[1], nil | ||
} |
Oops, something went wrong.