github.com/kubeshop/testkube@v1.17.23/pkg/skopeo/client.go (about) 1 package skopeo 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "math/rand" 8 "regexp" 9 "strings" 10 "time" 11 12 corev1 "k8s.io/api/core/v1" 13 14 "github.com/kubeshop/testkube/pkg/process" 15 ) 16 17 // DockerAuths contains an embedded DockerAuthConfigs 18 type DockerAuths struct { 19 Auths DockerAuthConfigs `json:"auths"` 20 } 21 22 // DockerAuthConfigs is a map of registries and their credentials 23 type DockerAuthConfigs map[string]DockerAuthConfig 24 25 // DockerAuthConfig contains authorization information for connecting to a registry 26 // It mirrors "github.com/docker/docker/api/types.AuthConfig" 27 type DockerAuthConfig struct { 28 Username string `json:"username,omitempty"` 29 Password string `json:"password,omitempty"` 30 Auth string `json:"auth,omitempty"` 31 32 // Email is an optional value associated with the username. 33 // This field is deprecated and will be removed in a later 34 // version of docker. 35 Email string `json:"email,omitempty"` 36 } 37 38 // DockerImage contains definition of docker image 39 type DockerImage struct { 40 Config struct { 41 User string `json:"User"` 42 Entrypoint []string `json:"Entrypoint"` 43 Cmd []string `json:"Cmd"` 44 WorkingDir string `json:"WorkingDir"` 45 } `json:"config"` 46 History []struct { 47 Created time.Time `json:"created"` 48 CreatedBy string `json:"created_by"` 49 } `json:"history"` 50 Shell string `json:"-"` 51 } 52 53 // Inspector is image inspector interface 54 type Inspector interface { 55 Inspect(registry, image string) (*DockerImage, error) 56 } 57 58 type client struct { 59 dockerAuthConfigs []DockerAuthConfig 60 } 61 62 var _ Inspector = (*client)(nil) 63 64 // NewClient creates new empty client 65 func NewClient() *client { 66 return &client{} 67 } 68 69 // NewClientFromSecrets creats new client from secrets 70 func NewClientFromSecrets(imageSecrets []corev1.Secret, registry string) (*client, error) { 71 auths, err := ParseSecretData(imageSecrets, registry) 72 if err != nil { 73 return nil, err 74 } 75 76 return &client{dockerAuthConfigs: auths}, nil 77 } 78 79 // Inspect inspect a docker image 80 func (c *client) Inspect(registry, image string) (*DockerImage, error) { 81 args := []string{ 82 "--override-os", 83 "linux", 84 "inspect", 85 } 86 87 if len(c.dockerAuthConfigs) != 0 { 88 i := rand.Intn(len(c.dockerAuthConfigs)) 89 args = append(args, "--creds", c.dockerAuthConfigs[i].Username+":"+c.dockerAuthConfigs[i].Password) 90 } 91 92 config := "docker://" + image 93 if registry != "" { 94 config = registry + "/" + image 95 } 96 97 args = append(args, "--config", config) 98 result, err := process.Execute("skopeo", args...) 99 if err != nil { 100 return nil, err 101 } 102 103 var dockerImage DockerImage 104 if err = json.Unmarshal(result, &dockerImage); err != nil { 105 return nil, err 106 } 107 108 var shell string 109 for i := len(dockerImage.History); i > 0; i-- { 110 command := dockerImage.History[i-1].CreatedBy 111 re, err := regexp.Compile(`/bin/([a-z]*)sh`) 112 if err != nil { 113 return nil, err 114 } 115 116 shell = re.FindString(command) 117 if shell != "" { 118 break 119 } 120 } 121 122 dockerImage.Shell = shell 123 return &dockerImage, nil 124 } 125 126 // ParseSecretData parses secret data for docker auth config 127 func ParseSecretData(imageSecrets []corev1.Secret, registry string) ([]DockerAuthConfig, error) { 128 var results []DockerAuthConfig 129 for _, imageSecret := range imageSecrets { 130 auths := DockerAuths{} 131 if jsonData, ok := imageSecret.Data[".dockerconfigjson"]; ok { 132 if err := json.Unmarshal(jsonData, &auths); err != nil { 133 return nil, err 134 } 135 } else if configData, ok := imageSecret.Data[".dockercfg"]; ok { 136 if err := json.Unmarshal(configData, &auths.Auths); err != nil { 137 return nil, err 138 } 139 } else { 140 return nil, fmt.Errorf("imagePullSecret %s contains neither .dockercfg nor .dockerconfigjson", imageSecret.Name) 141 } 142 143 // If registry is not provided, extract it from the image name 144 if registry == "" { 145 registry = extractRegistry(imageSecret.Name) 146 } 147 // Determine if there is a secret for the specified registry 148 if creds, ok := auths.Auths[registry]; ok { 149 username, password, err := extractRegistryCredentials(creds) 150 if err != nil { 151 return nil, err 152 } 153 154 results = append(results, DockerAuthConfig{Username: username, Password: password}) 155 } else { 156 return nil, fmt.Errorf("secret %s is not defined for registry: %s", imageSecret.Name, registry) 157 } 158 } 159 160 return results, nil 161 } 162 163 // extractRegistry takes a container image string and returns the registry part. 164 // It defaults to "docker.io" if no registry is specified. 165 func extractRegistry(image string) string { 166 defaultRegistry := "https://index.docker.io/v1/" 167 parts := strings.Split(image, "/") 168 // If the image is just a name, return the default registry. 169 if len(parts) == 1 { 170 return defaultRegistry 171 } 172 // If the first part contains '.' or ':', it's likely a registry. 173 if strings.Contains(parts[0], ".") || strings.Contains(parts[0], ":") { 174 return parts[0] 175 } 176 return defaultRegistry 177 } 178 179 func extractRegistryCredentials(creds DockerAuthConfig) (username, password string, err error) { 180 if creds.Auth == "" { 181 return creds.Username, creds.Password, nil 182 } 183 184 decoder := base64.StdEncoding 185 if !strings.HasSuffix(strings.TrimSpace(creds.Auth), "=") { 186 // Modify the decoder to be raw if no padding is present 187 decoder = decoder.WithPadding(base64.NoPadding) 188 } 189 190 base64Decoded, err := decoder.DecodeString(creds.Auth) 191 if err != nil { 192 return "", "", err 193 } 194 195 splitted := strings.SplitN(string(base64Decoded), ":", 2) 196 if len(splitted) != 2 { 197 return creds.Username, creds.Password, nil 198 } 199 200 return splitted[0], splitted[1], nil 201 }